Core Concepts
conceptReactive 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
- ReadSource text of inline and same-origin sheets
- CompileReactive calls become
var(--rcss-…)splices; event rules become ordinary rules - RewriteThe same sheet is updated in place through the CSSOM
- BindMatching elements get one binding each
- 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).
.particle { transform: translateX(live(random(0px, 500px), 200ms)); }/* becomes */.particle { transform: translateX(var(--rcss-1)); }The reaction loop
- EventAn event rule such as
:on(click)or:on(keydown(Escape))matches - Mutation
--open: toggle(),--n: increment(1)… compute new values - StateThe nearest owner’s cell changes
- SubscribersOnly the declarations and selectors that read it re-evaluate
- 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);}| name | value | previous | changes |
|---|---|---|---|
| Waiting for the preview to start. | |||
- 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.
Feature selection policy
Every construct is classified before it ships. The rule: reduce the JavaScript people write, not increase the functions they must learn.
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.
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.