Attribute playground
State controls on one side, targets with derived attributes on the other, and a live table that shows the real DOM attribute values (read with CSS :has() selectors, no script).
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.
| name | value | previous | changes |
|---|---|---|---|
| Waiting for the preview to start. | |||
Every rule of focus: (0.6.0) in one interface: transitions only, user activation only, eligibility, document order, and the cases where focus must not move (no transition, a disabled target, focus or the click inside an open popover).
The popover attribute and popovertarget (open, light dismiss, Escape), tabindex="-1" on the headings, :focus-visible.
Moving focus after a state change (element.focus() in each click handler), and keeping it inside an open popover.
tabindex="-1"), never by the library./* ---- state ---------------------------------------------------------------------------------- */.flow { --panel: state(1); --more: state(false); --locked: state(false); }.flow .go1 { on: click(set(--panel, 1)); }.flow .go2 { on: click(set(--panel, 2)); }.flow .go3 { on: click(set(--panel, 3)); }.flow .next { on: click(increment(--panel)); attr: disabled when(--panel >= 3); }.flow .prev { on: click(decrement(--panel)); attr: disabled when(--panel <= 1); }.flow .more { on: click(toggle(--more)); attr: aria-expanded var(--more); }.flow .lock { bind: --locked; }.flow .status .n { text: var(--panel); }/* ---- visibility: one panel at a time; the extra details are hidden until asked for ------------ */.flow .panel { display: none; }.flow .panel:nth-var(--panel) { display: block; }.flow .extra { attr: hidden when(--more, false, true); }.flow .confirm { attr: disabled var(--locked); }/* ---- focus (focus:, 0.6.0) ------------------------------------------------------------------- Each target is true in exactly one state, so only a change to that state moves focus. */.flow .p1 h2 { focus: when(--panel == 1); }.flow .p2 h2 { focus: when(--panel == 2); }/* Panel 3 focuses Confirm, not its heading. Locked = disabled = not eligible: focus is unchanged. */.flow .confirm { focus: when(--panel == 3); }/* Revealed and focused together: the hidden attribute is removed before focus is resolved. */.flow .extra h3 { focus: var(--more); }Source: the library's examples/focus-transitions/. 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.
State controls on one side, targets with derived attributes on the other, and a live table that shows the real DOM attribute values (read with CSS :has() selectors, no script).
Three nested boxes, each owning --n. Buttons change the nearest owner (or an explicit owner with closest()); a paragraph without its own state reads the nearest owner's value. Labels, chips and markers show which declaration the cascade chose: specificity, a :when() condition, @layer order and !important.
An order form: a bound name with live text, pickup or shipping (the address field appears and becomes required for shipping), a quantity with native min/max validity, a terms checkbox that enables submit, native :valid/:invalid/:user-invalid styling and native reset. method="dialog": nothing is sent or stored.