Reactive CSSexperimental
/*v0.9.0Declarative behavior for CSS*/

Add state, events and DOM binding to the stylesheet you already write. One small runtime reads your CSS and wires it up.

v0.9.0 · one script · no build step
marker.cssidle
.marker {
--x: state(0px);
--y: state(0px);
translate: var(--x) var(--y);
}
.marker:on(drag(move)) {
--x: set(event(x));
--y: set(event(y));
}
Drag the dot, tap anywhere in the field, or focus the dot and use the arrow keys.
/*01New in v0.9.0*/

Events, expressed as CSS.

Events are selectors. State, attributes and text are custom properties. Here is one disclosure menu, translated line by line.

JavaScriptReactive CSS
Statelet open = false;.home { --open: state(false); }
Eventbutton.addEventListener('click', () => {.nav button:on(click) {
Updateopen = !open;--open: toggle();
Attributebutton.setAttribute('aria-expanded', open);--attr-aria-expanded: var(--open);
Stylemenu.classList.toggle('is-open', open);.home:when(--open) .menu { display: block; }
Show the assembled stylesheet
.home {
--open: state(false);
}
.nav button {
--attr-aria-expanded: var(--open);
}
.nav button:on(click) {
--open: toggle();
}
.home:when(--open) .menu {
display: block;
}

JavaScript is still the right tool for data, routing and application logic. Reactive CSS covers the interactions in between.

/*03How it works*/

State belongs in your styles.

Every rule follows the same path. An event updates a state, the state reaches the DOM, and ordinary CSS styles the result.

  1. Event:on(click)Something happens to an element. The selector names the trigger.waiting for clickclick on button
  2. State--open: toggle()The declaration updates a state owned by the nearest matching element.--open: false
  3. DOM--attr-aria-expandedBound properties write attributes and text, so assistive tech sees the change.aria-expanded="false"
  4. Style:when(--open)Plain CSS takes over. Selectors match, transitions run..menu → display: none
Same menu as section 01. Both stay in sync.
/*04Runtime*/

Take only what you use.

Three builds of one runtime. Full and Compact have every module; Compact keeps a shorter feature registry. Custom is yours to choose.

49.342.59/9kB gzipkB gzipmodulesreactive-css.min.js, measured from the local v0.9.0 buildreactive-css.compact.min.js, measured from the local v0.9.0 buildSize is measured when the Bundle Builder builds it.
Open the Bundle Builder →
The library's 15 catalog features, in nine groups. Full and Compact include every one.Click modules to add or remove them. Core is always included; the Bundle Builder adds what a feature requires.
/*05Native first*/

Use CSS where CSS is enough. Extend it only when it isn't.

Native CSS · no runtime involved
:hover:focus-visible:checked:has()@keyframestransition@container<details>popover
Reactive CSS · added by the runtime
state():on()event()persist()bind():when()--attr-*

If :has() or :checked already does the job, use it. The runtime only handles what the browser doesn't understand, and without it the page falls back to plain CSS.

Read Simplify CSS →
/*06What comes next*/

CSS is evolving. We're exploring what comes next.

Browsers keep adding state-like features. Reactive CSS tries the ones that aren't there yet, in syntax that should feel familiar if they arrive. Below, a live specimen sheet: every value is reading this page right now.

sourcemouse()move over this sectionx · y Pointer position, read live.
sourceelement() × (this cell)Size of an element, kept up to date as it resizes.
sourceviewport() × Size of the viewport.
sourcevisible()truefalseWhether an element is on screen.
sourcelive()Keeps a value updating over time: a new random() every second.
historyprevious()previous(--count) → 3The value before the last change. Uses the counter in 2.1.
historydirection()0 no change yetWhether the last change went up or down.
conditionwhen()when(--count > 5, "many", "few") → "few"Picks one of two values from a condition.
selector:when().home:when(--count > 5) → truefalseMatches while a condition holds.
selector:changed()waiting for --countmatching nowMatches right after a value changes.
/*07Experiments*/

Experi­ments.

Curated demos from our lab, where Reactive CSS drives light, depth and motion instead of menus and counters. Each one is a stylesheet and the runtime.

001 · featuredChromium · WebKit

Perihelion · motion card

A foil collector card: 3D tilt, trailing specular light, parallax layers and a spring return.

  • mouse(local)
  • element()
  • :on(pointerenter)
  • cycle()
  • :changed()
  • event()
move over the card · click to change edition
/*08Tooling*/

Your AI agent speaks Reactive CSS.

Coding agents that speak the Model Context Protocol can use the official @reactivecss/mcp server: current 0.9.0 docs, API lookup, validation and migration help, so generated CSS uses the syntax that actually runs.

  • MCP Inspector · verified
  • other MCP clients · not yet tested
Explore MCP integration →
agent sessionillustrative
› validate menu.css
! on: click(toggle(--open)) legacy 0.8
! attr: aria-expanded … legacy 0.8
rewrite
.nav button:on(click) { --open: toggle(); }
.nav button { --attr-aria-expanded: var(--open); }
✓ valid · custom bundle: core, events, attributes
7 tools8 resources4 promptson npm
/* end of stylesheet */

Start with one script.

index.html
<script src="reactive-css.min.js" defer></script>
<link rel="stylesheet" href="styles.css">