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.

Live previewreal runtime
View source

How it works

  • 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-expanded var(--open); }.center .body { attr: hidden when(--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-pressed var(--r1); }.center .n2 .read { on: click(toggle(--r2)); attr: aria-pressed var(--r2); }.center .n3 .read { on: click(toggle(--r3)); attr: aria-pressed var(--r3); }.center .n1 { attr: data-read var(--r1); attr: hidden var(--d1); }.center .n2 { attr: data-read var(--r2); attr: hidden var(--d2); }.center .n3 { attr: data-read var(--r3); attr: hidden var(--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: disabled when(--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: disabled when(--d1 and --d2 and --d3); }.center .empty { attr: hidden when(--d1 and --d2 and --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.

Related

Keyboard playground

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.

0.8 syntaxfocusin()focusout()prevent()attr:

Focus transitions

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.

0.8 syntaxset()increment()decrement()toggle()

Keyboard navigation

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.

0.8 syntaxkey()prevent():when()cycle()