Documents v0.9.0 · previews run the real runtime · experimental features are marked exp
Notification center
A collapsible list of three sample notifications. Each can be marked read (a toggle with aria-pressed) or dismissed. "Undo dismiss" restores the last dismissed one, "Dismiss all" clears the list, and the unread count updates as real text.
toggle()lines 8, 13, 14, 15Flips a boolean state. In an :on() rule it is the mutation --open: toggle().
set()lines 19, 20, 21, 25…Sets a state to a value. In an :on() rule it is a mutation of the state on the left, --x: set(5), with typed expressions.
:when()lines 25, 26, 27Matches elements for which the condition is true, with states resolved from that element.
attr:lines 8, 9, 13, 14…Derives one allowlisted HTML attribute of the matched element, such as disabled or aria-expanded, from a reactive value.
A disclosure (aria-expanded + hidden), per-item ARIA toggle state and visibility derived from container-owned state, a one-step history (Undo) without arrays, and a count computed by native math from boolean states.
Left to native HTML and CSS
hidden, [data-read] styling, :disabled.
The count: a boolean state is 1/0 in CSS, so calc((1 - read) * (1 - dismissed) + …) counts unread items; a registered @property integer makes the browser compute it.
What would normally need JavaScript
Everything: dismissing, undo, read state, the count and the disclosure. <details> could replace the disclosure natively, but not the rest.
Limitations
A fixed list. Three notifications, one state pair each, owned by the center (the count and “Dismiss all” need them all). Real notifications arrive dynamically: that needs script.
One-step history.--last remembers only the last dismissal; Undo twice restores one item. A full history needs a list, and persistent arrays are out of scope.
“Dismiss all” clears the history (--last = 0): Undo cannot restore a bulk dismissal.
Focus. Dismissing hides the focused Dismiss button, and focus returns to the page. focus: (0.6.0) cannot fix this: it reacts to a condition becoming true, and “the last dismissed item changed” stays true from the second dismissal on. It is left as a documented limitation rather than forcing a rule that works only for the first dismissal.
Rules repeat per item.
Accessibility
Disclosure with real aria-expanded and aria-controls; the collapsed list is hidden (tested).
“Mark as read” is a toggle button with real aria-pressed (tested); the unread count is DOM text in a polite live region (announcements not verified).
Dismissed items are hidden: removed from the accessibility tree.
This is not the ARIA feed or log pattern, and new notifications are not announced (there are none: the list is static).
Implementation
2 files · 35 lines
style.cssreactive
/* ---- state: owned by the center ---------------------------------------------------------------- One read and one dismissed flag per notification (no arrays), whether the list is open, and which notification was dismissed last (0 = none): a one-step history for Undo. */.center{--open:state(true);--r1:state(false);--r2:state(false);--r3:state(false);--d1:state(false);--d2:state(false);--d3:state(false);--last:state(0);}/* ---- expand / collapse: a disclosure (attr:, 0.5.3) ------------------------------------------- */.center.toggle{on:click(toggle(--open));attr:aria-expandedvar(--open);}.center.body{attr:hiddenwhen(--open,false,true);}/* ---- per notification: read state as aria-pressed on its toggle, data-read on the item, and the item hidden once dismissed. One rule per item: no parameterized rules. */.center.n1.read{on:click(toggle(--r1));attr:aria-pressedvar(--r1);}.center.n2.read{on:click(toggle(--r2));attr:aria-pressedvar(--r2);}.center.n3.read{on:click(toggle(--r3));attr:aria-pressedvar(--r3);}.center.n1{attr:data-readvar(--r1);attr:hiddenvar(--d1);}.center.n2{attr:data-readvar(--r2);attr:hiddenvar(--d2);}.center.n3{attr:data-readvar(--r3);attr:hiddenvar(--d3);}.center.n1.dismiss{on:click(set(--d1,true),set(--last,1));}.center.n2.dismiss{on:click(set(--d2,true),set(--last,2));}.center.n3.dismiss{on:click(set(--d3,true),set(--last,3));}/* ---- history: Undo brings back the last dismissed notification (one step) ------------------- */.center.undo{attr:disabledwhen(--last==0);}.center:when(--last==1).undo{on:click(set(--d1,false),set(--last,0));}.center:when(--last==2).undo{on:click(set(--d2,false),set(--last,0));}.center:when(--last==3).undo{on:click(set(--d3,false),set(--last,0));}.center.dismiss-all{on:click(set(--d1,true),set(--d2,true),set(--d3,true),set(--last,0));attr:disabledwhen(--d1and--d2and--d3);}.center.empty{attr:hiddenwhen(--d1and--d2and--d3,false,true);color:#555;}/* ---- unread count: booleans are 1/0 in CSS, so native math counts them ---------------------- */@property--unread{syntax:"<integer>";inherits:true;initial-value:0;}.center{--unread:calc((1-var(--r1))*(1-var(--d1))+(1-var(--r2))*(1-var(--d2))+(1-var(--r3))*(1-var(--d3)));}.center.count.n{text:var(--unread);}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →
Source: the library's examples/notification-center/. style.css from its first section comment; the layout above it runs but is not shown. Written in the 0.8 syntax (on:, bind:, text:, attr:), which 0.9.0 still runs; see Migrating from 0.8 for the 0.9 forms.
A focusable pad moved with the arrow keys (Escape centers it), focus tracking with focusin/focusout, nested regions that show how one key press reaches several handlers, and a Reset button that is disabled while there is nothing to reset.
Three panels switched by buttons, Previous/Next and a native popover menu. Focus follows user-initiated panel changes to the new panel's heading (or to Confirm on panel 3); a hidden section is revealed and focused in one action; a locked (disabled) Confirm and the open popover deliberately keep focus where it is.
A listbox-like list on a deliberately tall page. Arrow keys, Home and End move the selection and focus (roving tabindex) and are cancelled with prevent(), so the page does not scroll; a lock makes the list's key rule inactive, and the keys scroll the page again. A nested region inside one item has its own keys, and a filter field in the same section keeps its native keys.