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

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

Live previewreal runtime
View source

How it works

  • attr:lines 11, 12, 14, 17…Derives one allowlisted HTML attribute of the matched element, such as disabled or aria-expanded, from a reactive value.
  • bind:lines 5, 6, 7, 8Connects a native form control two-way with the nearest state of that name.
  • on:lines 4, 11, 14, 17…Connects 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.
  • :when()line 18Matches elements for which the condition is true, with states resolved from that element.

Every allowlisted attribute kind in one place, with the exact expressions: attr: aria-expanded var(--open), attr: disabled when(--locked), attr: aria-disabled when(--busy), attr: data-state var(--status), attr: tabindex when(--selected, 0, -1), and --open2: state(false, bind(aria-expanded)). The difference between native disabled and aria-disabled.

Left to native HTML and CSS

:has() + attribute selectors for the live table; disabled blocking clicks and removing the button from the Tab order; hidden.

What would normally need JavaScript

Setting every attribute by hand in event handlers, keeping them consistent.

Limitations

  • when(--selected, 0, -1) gives the number tokens 0/-1, which tabindex accepts; other values are refused.
  • The aria-disabled button needs the extra :when() rule: the attribute itself blocks nothing.
  • The live table shows absent attributes as “(absent)”/“absent”.

Accessibility

  • disabled: announced as unavailable, skipped by Tab, clicks blocked natively.
  • aria-disabled="true": announced as unavailable, still focusable and clickable; the action is made inactive by CSS. Not verified with screen readers.

Implementation

2 files · 43 lines
style.cssreactive
/* ---- states ------------------------------------------------------------------------------------ */.p { --open: state(false); --locked: state(false); --busy: state(false); --status: state(idle); --selected: state(false);  --submits: state(0); --saves: state(0); }.p .t-open { on: click(toggle(--open)); }.p .c-locked { bind: --locked; }.p .c-busy { bind: --busy; }.p .c-status { bind: --status; }.p .c-selected { bind: --selected; }/* ---- derived attributes (attr:) -------------------------------------------------------------------- */.p .disclosure { attr: aria-expanded var(--open); on: click(toggle(--open)); }.p .region { attr: hidden when(--open, false, true); }/* Native disabled: the browser blocks clicks and skips the button in the Tab order. */.p .submit { attr: disabled when(--locked); on: click(increment(--submits)); }/* aria-disabled: announced as unavailable, still focusable and clickable. Nothing is blocked by the   attribute: a more specific rule makes the action inactive while --busy. */.p .save { attr: aria-disabled when(--busy); on: click(increment(--saves)); }.p:when(--busy) .save { on: click(set(--busy, true)); }.p .card { attr: data-state var(--status); }.p .item { attr: tabindex when(--selected, 0, -1); }.p .submits { text: var(--submits); }.p .saves { text: var(--saves); }/* ---- an attribute that owns its state: state(initial, bind(attribute)) -------------------------- */.p .bound { --open2: state(false, bind(aria-expanded)); on: click(toggle(--open2)); }/* ---- live attribute table: native :has() over the real DOM attributes (no script) ---------------- */.d-exp::after, .d-bound::after, .d-aria::after { content: "(absent)"; color: #999; }.p:has(.disclosure[aria-expanded="true"]) .d-exp::after { content: '"true"'; color: inherit; }.p:has(.disclosure[aria-expanded="false"]) .d-exp::after { content: '"false"'; color: inherit; }.d-hidden::after { content: "absent"; } .p:has(.region[hidden]) .d-hidden::after { content: "present"; }.d-disabled::after { content: "absent"; } .p:has(.submit[disabled]) .d-disabled::after { content: "present"; }.p:has(.save[aria-disabled="true"]) .d-aria::after { content: '"true"'; color: inherit; }.p:has(.save[aria-disabled="false"]) .d-aria::after { content: '"false"'; color: inherit; }.d-state::after { content: "(absent)"; }.p:has(.card[data-state="idle"]) .d-state::after { content: '"idle"'; }.p:has(.card[data-state="loading"]) .d-state::after { content: '"loading"'; }.p:has(.card[data-state="done"]) .d-state::after { content: '"done"'; }.d-tab::after { content: "(absent)"; }.p:has(.item[tabindex="0"]) .d-tab::after { content: '"0"'; }.p:has(.item[tabindex="-1"]) .d-tab::after { content: '"-1"'; }.p:has(.bound[aria-expanded="true"]) .d-bound::after { content: '"true"'; color: inherit; }.p:has(.bound[aria-expanded="false"]) .d-bound::after { content: '"false"'; color: inherit; }
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/attribute-playground/. 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

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

History and motion

A level from 0 to 20 with Up, Down, +5 and Reset. The previous value and the direction of the last change are shown as text; the bar width transitions natively; the value pulses (or dips) on every change via :changed(). Reduced motion turns both off.

0.8 syntaxincrement()decrement()set()previous()

Product configurator

Two product cards. Each has a quantity (1–10, buttons and a number field), a color (radio group), gift wrapping (checkbox), a live total (real DOM text, 0.5.1) and a native reset. It is a local UI: no checkout, no network requests, nothing stored.

0.8 syntaxbind:increment()decrement()attr: