Documents v0.9.0 · previews run the real runtime · experimental features are marked exp

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.

Live previewreal runtime
View source

How it works

  • set()lines 3, 4, 5Sets 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.
  • increment()line 6Adds a step to a number or dimension state. In an :on() rule it is the mutation --count: increment(1).
  • decrement()line 7Subtracts a step from a number or dimension state. In an :on() rule it is the mutation --count: decrement(1).
  • toggle()line 8Flips a boolean state. In an :on() rule it is the mutation --open: toggle().

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).

Left to native HTML and CSS

The popover attribute and popovertarget (open, light dismiss, Escape), tabindex="-1" on the headings, :focus-visible.

What would normally need JavaScript

Moving focus after a state change (element.focus() in each click handler), and keeping it inside an open popover.

Limitations

  • Disabled targets are skipped, not retried. Locking Confirm and then going to panel 3 leaves focus where it was. Unlocking later does not move focus.
  • Focus after a click differs by browser (see section 9), but never lands behind an open popover.
  • No focus restoration when the popover closes: the browser decides (native behavior).

Accessibility

  • Headings are made focusable by the author (tabindex="-1"), never by the library.
  • Moving focus to a heading after a step change is a common pattern, but whether screen readers announce it well is not verified.

Implementation

2 files · 25 lines
style.cssreactive
/* ---- 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); }
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/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.

Related

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).

0.8 syntaxattr:bind:on::when()

Nested state and cascade

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.

0.8 syntaxtext:attr::when()

Form interactions

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.

0.8 syntaxbind:text:attr::when()