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

Core Concepts

concept

Reactive CSS compiles its syntax extensions into ordinary CSS and custom properties, then keeps those properties up to date. This page explains the pipeline, the reaction loop, and where native CSS takes over.

On this page · 7 sections+

From stylesheet to page

  1. ReadSource text of inline and same-origin sheets
  2. CompileReactive calls become var(--rcss-…) splices; event rules become ordinary rules
  3. RewriteThe same sheet is updated in place through the CSSOM
  4. BindMatching elements get one binding each
  5. UpdateA shared scheduler writes only changed values

The compiled sheet keeps its position, media, disabled state and entry in document.styleSheets. Nothing else in the text changes. The runtime reads the source text because the browser drops what it does not understand, such as the selector :on(click).

compiled.cssreactive
.particle { transform: translateX(live(random(0px, 500px), 200ms)); }/* becomes */.particle { transform: translateX(var(--rcss-1)); }
live()Function · hover any red tokenRe-evaluates an expression on an interval.Reference →

The reaction loop

  1. EventAn event rule such as :on(click) or :on(keydown(Escape)) matches
  2. Mutation--open: toggle(), --n: increment(1) … compute new values
  3. StateThe nearest owner’s cell changes
  4. SubscribersOnly the declarations and selectors that read it re-evaluate
  5. BrowserStyle, transitions and paint happen natively

Every interactive example in these docs shows this loop: the trigger line lights first, then the state line, then its dependents.

.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
state
namevaluepreviouschanges
Waiting for the preview to start.
events
  1. Interact with the preview to record events.

Each DOM event is one transaction. Every mutation reads the values from before the event, then all writes are applied together, so two mutations never see each other’s half-finished result. When several rules on an element change the same state for the same event, the cascade picks one winner, as it does for any property.

Four kinds of syntax

Values

Functions on the right side of a declaration: state(), when(), previous(), direction(), random(), pick(), live(), on(), mouse(), visible(), viewport(), element() and typed(). You can nest them: live(pick(red, blue), 1s) or calc(0.2 + visible() * 0.8). state() takes the modifiers bind() and persist().

Event rules

.button:on(click) { --open: toggle(); } runs mutations when an event happens. Selectors say when, declarations say what: an :on() rule contains only state changes, and styles stay in ordinary rules. See Events & Actions.

DOM integration

--attr-* writes attributes, content on an element writes its DOM text, and focus: moves focus after a user action. All of them cascade like any declaration. See DOM Integration and Focus & Blur.

Selectors

:when(), :nth-var(), :changed(), :just-checked, :just-unchecked and :blur compile to attribute selectors with the same specificity as one pseudo-class (or :nth-child() for :nth-var()).

Native first

Work the browser can do is compiled to plain CSS and costs nothing at runtime.

FeatureRuns as
viewport(width)Native 100dvw
state() initial valuesNative var() fallback, no write until the state changes
when() on boolean statesNative if() where supported, else the runtime
Top-level random()Native random() when the browser supports the call as written and gives per-element values
content on ::before and ::after, literal contentNative generated content, untouched
:focus, :focus-visible, :focus-withinNative, untouched
Everything elseOne delegated listener per event type and per-element work only for subscribers

Feature selection policy

Every construct is classified before it ships. The rule: reduce the JavaScript people write, not increase the functions they must learn.

CategoryMeaningExamples
Keep nativeNever duplicated or reinterpretedcalc(), progress(), hypot(), :has(), sibling-index(), anchor-size(), if(), @container, popover, <details>
SimplifyShorter syntax with equivalent behaviorwhen(), viewport(), typed()
ExtendWhat CSS cannot dostate, event rules, bindings, history, stateful selectors, element(), drag gestures, persist()
EvaluateNot built without a concrete case and a native gapphysics, network, DOM mutation

Updates and cost

A state change re-evaluates only that cell’s subscribers, in an urgent lane that is never deferred. Other updates are coalesced into one read pass and one write pass per animation frame, with a budget of about 4 ms of script time.

Note · What updates cost

Writing a custom property doesn’t rebuild the DOM, but the browser still recalculates style, and may need layout and paint. Prefer translate, scale, rotate and opacity for values that change often.

Experimental · syntax may change between minor versions