Simplify CSS
conceptReactive 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
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.
@property --rotation { syntax: "<angle>"; inherits: false; initial-value: 0deg;}.card { rotate: var(--rotation); transition: --rotation 400ms;}.card:hover { --rotation: 90deg;}.card { --rotation: typed(0deg); rotate: var(--rotation); transition: --rotation 400ms;}.card:hover { --rotation: 90deg;}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.
- 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.
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.
.card { --theme: light; background: if( style(--theme: dark): #171717; else: #ffffff );}/* something else must set the class */.card.is-dark { --theme: dark;}.card { --theme: state(light); background: when( --theme == dark, #171717, #ffffff );}.card .mode:on(click) { --theme: cycle(light, dark);}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.
- 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@mediarule. - 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 > 3or 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.
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.free-shipping .banner { opacity: 1; translate: 0;}.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;}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.
- 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.
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.
Example: native first
.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.