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

Simplify CSS

concept

Reactive CSS has two purposes. It extends CSS with behavior the platform doesn’t provide, and it simplifies a few native patterns that take more syntax than they need. Less syntax, same CSS philosophy.

On this page · 8 sections+

Extend and simplify

Extend
Add what CSS cannot do: state, event rules, bindings, history, element()
Simplify
Shorter syntax for an existing capability: typed(), when(), viewport()
Keep native
Everything the platform already does well: calc(), :has(), if(), @container

Modern CSS can do a lot: conditional values, typed custom properties, container queries, powerful math, relational selectors and native animation. Some everyday patterns still take a surprising amount of boilerplate. For a few of them, Reactive CSS offers a shorter form.

A shorter form is not automatically better. Each comparison below says how the two versions relate, what actually gets simpler, and where they differ.

How to read a comparison

LabelMeaning
Identical behaviorSame result in every context. Reactive CSS compiles to the native form.
Approximately equivalentSame result in the common case, with documented semantic differences.
Overlapping, not interchangeableBoth solve some of the same problems through different mechanisms.
Reactive CSS onlyNative CSS has no direct equivalent. The comparison is against a script.

viewport(width) is the one identical case: it compiles to native 100dvw. Because it saves almost nothing on its own, it’s only worth using nested inside another function, as in random(0px, viewport(width)).

Typed custom properties

@property registers a custom property with a type, so the browser can interpolate and compute it. That takes a five-line at-rule, usually far from the rule that uses it. typed() infers the type from the initial value.

Typed custom propertiesApproximately equivalentexperimental
Native CSS · card.css@property
@property --rotation {  syntax: "<angle>";  inherits: false;  initial-value: 0deg;}.card {  rotate: var(--rotation);  transition: --rotation 400ms;}.card:hover {  --rotation: 90deg;}
Reactive CSS · card.csstyped()
.card {  --rotation: typed(0deg);  rotate: var(--rotation);  transition: --rotation 400ms;}.card:hover {  --rotation: 90deg;}
Resulthover only
What becomes simpler

The syntax is inferred from the initial value, so one declaration replaces the five-line @property block, and the registration sits next to the rule that uses it.

Differences and compatibility
  • typed() registers with inherits: true. This @property uses inherits: false. When inheritance matters, write @property yourself.
  • Both are document-wide and permanent. typed() uses CSS.registerProperty(), the same registration @property performs.
  • typed() never overrides a native @property for the same name in a readable stylesheet.
  • typed() needs the runtime. Without it, --rotation stays an unregistered property and the rotation jumps instead of animating.
  • typed() is experimental. @property is native in current Chromium, WebKit and Firefox releases.
Experimental · typed()

typed() is experimental. Prefer @property when you need inherits: false, a non-inferable syntax such as <length-percentage>, or a page that must work without the runtime.

Conditional values

Native if() picks a value from style(), media() or supports() queries. when() picks a value from Reactive CSS state. They overlap when the condition is a custom property, but they answer different questions.

Conditional valuesOverlapping, not interchangeablestable
Native CSS · card.cssif()
.card {  --theme: light;  background: if(    style(--theme: dark): #171717;    else: #ffffff  );}/* something else must set the class */.card.is-dark {  --theme: dark;}
Reactive CSS · card.csswhen()
.card {  --theme: state(light);  background: when(    --theme == dark,    #171717,    #ffffff  );}.card .mode:on(click) {  --theme: cycle(light, dark);}
Resultnative button
What becomes simpler

The value that the condition reads is also owned and changed in CSS. There is no class to toggle from a script, and when() accepts comparisons such as --count > 3.

Differences and compatibility
  • Native if() tests style(), media() and supports() queries. when() tests Reactive CSS state, comparisons and logical operators.
  • With native if(), something still has to change --theme: a class, an attribute or a script.
  • Conditions made only of boolean states compile to native if() where the browser supports it. This one compares a keyword, so it runs in the runtime.
  • An unknown when() condition gives no value. A native if() with no matching branch and no else is invalid at computed-value time.
  • If a condition depends only on a media query, keep it native: if(media(…)) or @media.
  • Native if() is not available in every browser yet. Check support before relying on it.

When to keep if() native

  • The condition is a media query or a feature query. Write if(media(…)), if(supports(…)) or an @media rule.
  • The custom property is set by your own script or markup, and nothing in CSS needs to change it.

When when() helps

  • CSS also owns and changes the value, through state() and event rules such as .b:on(click).
  • You need a comparison such as --count > 3 or a combination such as --open and not --pinned.

Conditional style blocks

:when() applies a whole rule while a condition is true. The useful case is a condition native selectors can’t see, such as a counter crossing a threshold.

Conditional style blocksReactive CSS onlystable
Native CSS · cart.jsscript + class
const cart = document  .querySelector('.cart');let items = 1;const add = cart.querySelector('.add');add.addEventListener('click', () => {  items++;  cart.classList.toggle(    'free-shipping',    items >= 3  );});
cart.css
.cart.free-shipping .banner {  opacity: 1;  translate: 0;}
Reactive CSS · cart.css:when()
.cart {  --items: state(1);}.cart .add:on(click) {  --items: increment(1);}.banner {  opacity: 0;  translate: 0 8px;  transition:    opacity 200ms,    translate 200ms;}.cart:when(--items >= 3) .banner {  opacity: 1;  translate: 0;}
Resultnative button
What becomes simpler

The threshold is part of the selector. The count, the rule that changes it and the styles that depend on it are in one place, with no class name shared between files.

Differences and compatibility
  • Native CSS has no counter state, so this is not a shorter spelling of an existing feature. It replaces the script that counts and toggles a class.
  • Native style queries such as @container style(--free-shipping: true) can match a custom property, but they need a container ancestor, and something else must still count and set the value.
  • :when() compiles to an attribute selector with the specificity of one pseudo-class.
  • For state the platform already tracks, such as a checked box, :checked and :has() are simpler. Use those.
  • An unknown condition, for example a missing state, makes :when() not match and logs a warning.
Note

Not a replacement for :checked: If the state is something the platform already tracks, such as a checked box, an open <details> or a focused field, use :checked, [open], :focus-within or :has(). They need no runtime.

When native CSS is already simpler

Reactive CSS does not add alternatives for things CSS already expresses well. Duplicates would mean two ways to do the same thing, two behaviors to learn, and runtime work the browser can do for free.

NeedNative CSSWhy there is no Reactive CSS version
Arithmeticcalc(var(--gap) * 2)Already concise, and computed by the browser
Fluid sizesclamp(1rem, 2.5vw, 2rem)One function, no runtime
Mapping a rangeprogress(var(--x), 0px, 600px)The proposed map() was rejected in favor of it
Distancehypot(var(--dx), var(--dy))The proposed distance() was rejected in favor of it
Mixing colorscolor-mix(in oklch, red 40%, white)Already declarative and typed
Light and darklight-dark(#fff, #171717)Follows color-scheme with no state
Parent and sibling state.card:has(input:checked)The platform already tracks it
Disclosure, popups, dialogs<details>, popover, <dialog>Accessible by default
Form state:checked, :invalid, :user-validNative controls know their own state

Example: native first

card.css
.card {  color-scheme: light dark;  background: light-dark(#ffffff, #171717);  padding: clamp(12px, 3vw, 24px);}.card:has(input:checked) {  outline: 2px solid color-mix(in oklch, #f2542d 60%, transparent);}

No part of this needs Reactive CSS. Reach for it when a value has to change in response to something the selectors above can’t express.

Compatibility

  • Reactive CSS syntax needs the runtime. Without it, browsers drop the unknown values and keep any static declarations before them.
  • Native if(), progress() and some newer selectors are not available in every browser. Check support for the native side of each comparison too.
  • Runtime behavior is tested in Chromium and WebKit through Playwright engine builds. Firefox is unverified for 0.9.0: its automated suite has not run. See Browser Compatibility.
Experimental · syntax may change between minor versions