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).
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-expandedvar(--open);on:click(toggle(--open));}.p.region{attr:hiddenwhen(--open,false,true);}/* Native disabled: the browser blocks clicks and skips the button in the Tab order. */.p.submit{attr:disabledwhen(--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-disabledwhen(--busy);on:click(increment(--saves));}.p:when(--busy).save{on:click(set(--busy,true));}.p.card{attr:data-statevar(--status);}.p.item{attr:tabindexwhen(--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.
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.
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.
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.