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

Accessible toggle button

A pressed-state toggle button and a disclosure button. Their aria-pressed and aria-expanded attributes are real DOM attributes, kept in sync with Reactive CSS state.

Live previewreal runtime
View source

How it works

  • state()lines 4, 9Makes each matching element the owner of an independent state with an initial value.
  • bind()lines 4, 9state(v, bind(value)) and state(v, bind(checked)) keep a form control and its own state equal in both directions. bind(<attribute>) reflects a state into one of its owner’s attributes.
  • on:lines 4, 9Connects triggers to the actions they run. A normal declaration: specificity, source order, @media and :hover decide which one applies. 0.8 syntax, still supported in 0.9.
  • click()lines 4, 9The events on: can listen for: click(), change(), input(), pointerenter(), pointerleave(), focus() and blur(), plus key() and outside().

state(initial, bind(attribute)) (0.5.2):

  • the button’s own attribute initializes the state (aria-pressed="true" starts pressed);
  • every state change writes the attribute, synchronously;
  • the styles use plain attribute selectors.
.toggle { --pressed: state(false, bind(aria-pressed)); on: click(toggle(--pressed)); }
.toggle[aria-pressed="true"] { background: #1d4ed8; color: #fff; }

Left to native HTML and CSS

  • The buttons: focus, keyboard activation (Enter, Space) and the accessible role come from native <button>.
  • The styling: the pressed look, the panel and the arrows all use attribute selectors on the real attributes ([aria-pressed="true"], [aria-expanded="true"] + .panel).
  • Native alternatives:
    • for a disclosure, <details>/<summary> needs no script and no library: prefer it when its look fits;
    • for a pressed toggle, no native element exists (a checkbox is a different control).

What would normally need JavaScript

Keeping aria-pressed and aria-expanded in sync with the visual state: one click listener per button that flips the attribute.

Limitations

  • The attribute belongs to the state’s owner. bind() only writes the owner’s own attribute: here, the button. The panel’s visibility follows from CSS, not from its own hidden attribute. Binding hidden on the panel would need the panel to own a state that the button’s click changes, and an action reaches only the clicking element and its ancestors.
  • One direction after start. A script that changes aria-expanded later is not read back; the next click overwrites it (reported once).
  • Not verified with real screen readers. The attributes are correct in the DOM and the accessibility tree (tested), but no VoiceOver or NVDA testing was done.

Accessibility

  • Native buttons with visible labels and visible focus.
  • Pressed and expanded states are exposed through ARIA attributes, the purpose of this example.
  • The ✓ and arrows are decorative (content: … / "" gives them empty alternative text).
  • aria-controls links the disclosure to its panel.

Implementation

2 files · 13 lines
style.cssreactive
/* ---- toggle: the state is bound to the button's own aria-pressed (0.5.2) ----------------------   The markup gives the initial value (aria-pressed="true" starts pressed). Each click writes the   attribute; native CSS styles it, and assistive technology reads it. */.toggle { --pressed: state(false, bind(aria-pressed)); on: click(toggle(--pressed)); }.toggle[aria-pressed="true"] { background: #1d4ed8; border-color: #1d4ed8; color: #fff; }.toggle[aria-pressed="true"]::after { content: " ✓" / ""; }/* ---- disclosure: the button's aria-expanded, and its panel next to it ------------------------ */.disclosure { --open: state(false, bind(aria-expanded)); on: click(toggle(--open)); }.disclosure + .panel { display: none; padding: 8px 0 0; }.disclosure[aria-expanded="true"] + .panel { display: block; }.disclosure::before { content: "▸ " / ""; }.disclosure[aria-expanded="true"]::before { content: "▾ " / ""; }
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/accessible-toggle/. 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

Shopping cart

Three cart lines with − / + / Remove, a subtotal per line, the item count and total, an "empty cart" message, and Checkout and "Empty cart" buttons that are disabled while the cart is empty. Local only: Checkout does nothing.

0.8 syntaxincrement()decrement()set()attr:

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.

0.8 syntaxtoggle()set():when()attr:

Form validation

A sign-up form using native constraint validation: required fields, type="email", minlength. A company field appears and becomes required only for business accounts. Submit is disabled until the terms are accepted, and error messages appear after the first submit attempt. Nothing is sent.

0.8 syntaxbind:attr:set():when()