State
conceptA state is a value an element owns. Declare it with state(), change it with mutations inside :on() rules, and read it with var(), when() or :when().
On this page · 10 sections+−
Declaring state
state() is only valid as the whole value of a custom property.
.player { --playing: state(false); --volume: state(0.8); --size: state(320px); --mode: state(compact); --title: state("Untitled");}The initial value sets the type for the state’s lifetime. Read a state with var(--x), when() or :when().
Ownership
Every element that matches a state() rule owns an independent state. Consumers, and the mutations that change it, use the nearest owner: the element itself, then its ancestors. An inner component with the same name shadows the outer one.
.item { --open: state(false);}.item > button:on(click) { --open: toggle();}/* Writes aria-expanded="true" or "false" on the button. */.item > button { --attr-aria-expanded: var(--open);}.item .body { display: none;}.item:when(--open) .body { display: block;}.item .chevron { rotate: when(--open, 180deg, 0deg);}Each .item above owns its own --open, so opening one item leaves the others closed.
Sharing
To share a state, declare it on a common ancestor. To make it page-wide, declare it on :root. Siblings never see each other’s states: in the counter below, --count is on .counter, so both buttons and the output can reach it.
Changing state
Inside an :on() rule, a declaration of the state’s own custom property is a mutation: it says how that state changes when the event happens.
.counter { --count: state(0);}.counter .inc:on(click) { --count: increment(1);}.counter .dec:on(click) { --count: decrement(1);}/* DOM text: the output's text node follows the state. */.counter .value { content: var(--count);}/* A real disabled attribute while the count is 0. */.counter .dec { --attr-disabled: when(--count <= 0);}The state on the left is always the one that changes. Mutations are only meaningful inside :on() rules.
One model
All five mutations work the same way as set(): compute a new value from the values before the event, then store it in the nearest owner. Conceptually, --count: increment(1) is --count: set(calc(var(--count) + 1)), and toggle() stores the opposite of the current boolean. The shorthands add checks and defaults: increment() without a step adds 1 in the state’s unit, while calc(var(--size) + 1) on a px state would mix a dimension with a plain number.
Types and units in mutations
A mutation stores a value only if it fits the state’s type:
- Units convert within a family: the absolute lengths (
px,cm,mm,in,pt…);deg,grad,rad,turn;sandms.increment(1cm)on apxstate adds about 37.8px. Percentages are their own unit. - Units that need layout, such as
em,remandvw, are not resolved. Useelement()orviewport()instead. - Mismatches write nothing.
increment(5%)on apxstate,set(10px)on a number state orset("dark")on a keyword state gives one diagnostic, and the state keeps its value. The same applies to dividing by zero. - A step needs the state’s unit: write
increment(10px)for apxstate, orincrement()for 1px. - Booleans count as 1 or 0 in arithmetic, as
var(--open)does incalc(). - Two shortcuts: a unitless
0can be stored in a dimension state, and0or1in a boolean state.
Expressions in mutations
set(), increment() and decrement() take an expression, evaluated once at the event.
.slider { --level: state(50);}.slider .more:on(click) { --level: set(min(var(--level) + 10, 100));}.slider .less:on(click) { --level: set(max(var(--level) - 10, 0));}There are no other operators or functions: no comparisons, no when() and no JavaScript. Anything else is a compile error that lists what is available. Inside a mutation, the functions in the last row are snapshots taken at the event, not live values: see Event data.
state() options
state() takes the initial value first, then up to two options in either order.
bind()
state(v, bind(value)) and state(v, bind(checked)) bind a form control to its own state, two-way. state(v, bind(aria-pressed)) reflects the state into an attribute. Details: Data Binding and bind().
.toggle { --on: state(false, bind(aria-pressed));}.toggle:on(click) { --on: toggle();}persist()
state(v, persist("key")) keeps the value in localStorage across reloads. Experimental. See Persistence.
:root { --dark: state(false, persist("theme"));}Initial value from an attribute
state(attr(name, fallback)) reads an attribute once, when the state is created, and never synchronizes it again. The fallback’s type is the state’s type: with attr(aria-expanded, false) the attribute is parsed as a boolean, and an invalid or missing value gives the fallback.
.disclosure { --open: state(attr(data-open, false));}state(bind(x)) without an initial value is rejected; write state(false, bind(aria-expanded)).
Printing values
On an element, content: var(--count) writes the state as the element’s text: numbers, strings and keywords alike. The element must be empty or hold only text. On ::before and ::after, content stays native CSS, which cannot print a number from a custom property; use a counter there:
.counter output::before { counter-reset: n var(--count); content: counter(n);}Lifetime
A state is created when its element starts matching a state() rule and is disposed when it stops matching or is removed. Re-inserting the element starts over at the initial value. CSS recalculation never resets a state.
State resets on reload and when its stylesheet is edited, unless it uses persist(). If another, non-state declaration of the same property wins the cascade, var(--x) shows that value while conditions and mutations keep using the state.
Upgrading from 0.8
Stylesheets written for 0.8 are covered in Migrating to 0.9.