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

State

concept

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

state.cssreactive
.player {  --playing: state(false);  --volume: state(0.8);  --size: state(320px);  --mode: state(compact);  --title: state("Untitled");}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →
TypeExamplesNotes
Booleantrue, falseReads as 1 or 0 through var(), so it works in calc()
Number0, 0.8, -3
Dimension320px, 45deg, 200ms, 50%Keeps its unit
Keywordcompact, dark, #0b57d0Identifiers and colors
String"Untitled", ""Quoted

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);}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →
Resultnative buttons, aria-expanded
waiting for interaction

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.

MutationEffect
--x: set(value)Stores the value of an expression
--x: toggle()Flips a boolean
--x: increment(step)Adds a step: default 1, in the state’s unit
--x: decrement(step)Subtracts a step
--x: cycle(a, b, c)Moves to the next listed value, wrapping around. From a value that is not listed, it moves to the first.
.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);}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →
Resultnative buttons
waiting for interaction

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; s and ms. increment(1cm) on a px state adds about 37.8px. Percentages are their own unit.
  • Units that need layout, such as em, rem and vw, are not resolved. Use element() or viewport() instead.
  • Mismatches write nothing. increment(5%) on a px state, set(10px) on a number state or set("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 a px state, or increment() for 1px.
  • Booleans count as 1 or 0 in arithmetic, as var(--open) does in calc().
  • Two shortcuts: a unitless 0 can be stored in a dimension state, and 0 or 1 in a boolean state.

Expressions in mutations

set(), increment() and decrement() take an expression, evaluated once at the event.

slider.cssreactive
.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));}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →
KindAvailable
ValuesNumbers, dimensions, percentages, quoted strings, true, false, keywords
Operators+, -, *, / and parentheses. As in calc(), + and - need spaces around them.
Mathcalc(), min(), max(), clamp(), round(), abs(), all typed as above
States and propertiesvar(--name) or var(--name, fallback): the nearest state, else the element’s computed custom property, else the fallback
The eventevent(field)
Read at the eventmouse(), element(), viewport(), attr(), pick(), random(), sibling-index(), sibling-count()

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.cssreactive
.toggle {  --on: state(false, bind(aria-pressed));}.toggle:on(click) {  --on: toggle();}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →

persist()

state(v, persist("key")) keeps the value in localStorage across reloads. Experimental. See Persistence.

theme.cssreactive
:root {  --dark: state(false, persist("theme"));}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →

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.cssreactive
.disclosure {  --open: state(attr(data-open, false));}
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →

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:

output.css
.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.

Note · In memory unless persisted

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.

Experimental · syntax may change between minor versions