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

Custom Bundles

guidenew

Ship only the Reactive CSS capabilities your project uses. Analyze your stylesheets or choose features, let the resolver add what they require, and build one file with the reactive-css CLI.

On this page · 8 sections+

Since 0.8.0 the runtime is modular; 0.9.0 has 15 features. Reactive CSS can analyze your stylesheets, determine which capabilities they need, and build a production bundle with those features and everything they depend on.

Automatic · analyzeThe analyzer reads the stylesheets and pages you pass it and lists the features they use.reactive-css analyze
Manual · selectYou name features or a profile. Anything they require is added for you.reactive-css build --features …
  • Introduced in v0.8.0
  • CLI: reactive-css
  • Build time only · needs esbuild
  • Nothing loaded at runtime
  • Works under strict CSP

Start with the standard bundle. It contains every feature and needs no tooling. Build a custom one when its size matters.

How it works

From your CSS to one file.

  1. Your CSSYou pass the stylesheets and pages you serve. The analyzer reads only those inputs.
  2. Feature analyzerreactive-css analyze compiles them with the library’s own compiler and lists the Reactive CSS features they use. Native CSS is left alone.
  3. Dependency resolverThe core is always included. Each feature adds what it requires: focus adds events.
  4. Custom bundlereactive-css.min.js, one file you host yourself, and a manifest that records the version, features and sizes.

For this stylesheet:

styles.cssreactive
.tabs { --tab: state(1); }.tab:on(click) { --tab: set(2); }.panel:when(--tab == 2) { display: block; }
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →

the analyzer reports:

npx -p @reactivecss/core -p esbuild reactive-css analyze styles.css
Stylesheets analyzed: 1Features used: events, conditionsRecommended: --features events,conditions

Quick start

Your first custom bundle.

Choose features by
  1. Install

    Install the package and esbuild. The CLI needs esbuild; the runtime never does. See Installation.

    The commands on this page name both packages with -p, so npx uses your installed copies, or fetches them when they are missing. Do not shorten them to npx reactive-css: without a local install, that runs an unrelated npm package of that name.

    terminal
    npm install --save-dev @reactivecss/core@0.9.0 esbuild
  2. Analyze your CSS

    Point the analyzer at your stylesheets, HTML files or the folder you deploy. It lists the features they use and recommends a build.

    terminal
    npx -p @reactivecss/core -p esbuild reactive-css analyze public/

    If it reports the analysis as incomplete, include the missing stylesheets or use the full runtime.

  3. Build

    Build the bundle. The CLI prints what it included and why, the size, and the path of the manifest.

    terminal
    npx -p @reactivecss/core -p esbuild reactive-css build --features events,conditions,focus --out public/reactive-css.min.js
    output
    Built public/reactive-css.min.jsFeatures: core, events, conditions, focusOptional, not included: forms (useful for focus)Excluded: gestures, persist, nth-var, history, forms, text, attributes, blur, values, dynamic, geometrySize: 85540 B minified (83.5 KB), 29370 B gzip, 26343 B brotliManifest: public/reactive-css.min.manifest.json
  4. Use the file

    Serve the file from your site and load it with a script tag. An iife bundle starts itself.

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

    Then open your pages and check ReactiveCSS.diagnostics.

How a selection resolves

illustration · the library’s catalog, nothing is built
Always
core
Selected by you
conditions, focus
Required automatically
events required by focus

forms is useful with focus but not required, so it is not added.

terminal · generated
npx -p @reactivecss/core -p esbuild reactive-css build --features conditions,focus --out public/reactive-css.min.js

Sizes, profiles and the analysis of pasted CSS are in the Bundle Builder. Configure a real bundle →

Features and dependencies

Every bundle contains the core.

Each other feature is a real module with the requirements below. Selecting a feature adds what it requires; a feature that is only useful with another one is never added automatically. The Bundle Builder makes the same selection with sizes.

coreCore & state
Syntax
--x: state(initial) · attr(name [, fallback]) / pick(…) / split(text, "d") in state() and set() · previous(--x) / direction(--x) as condition operands · ReactiveCSS.start/stop/refresh/stats/compile/describe/diagnostics
Requires
nothing · always included
Status
stable
Category
Core & State
eventsEvents & actions
Syntax
.x:on(event [, prevent()] [, stop()]) { --state: set(expr) | toggle() | increment(n) | decrement(n) | cycle(a, b) } · :on(keydown(Key)) / :on(outside(click)) · event(field), calc(), min(), max(), clamp(), round(), var(), mouse(), element(), viewport(), random() at event time · legacy: on: trigger(action, …), key(), outside(), prevent()
Requires
core
Status
stable
Category
Events & Actions
gesturesDrag gesturesexperimental
Syntax
:on(drag) / :on(drag(start | move | end | cancel)) · event(x | y | delta-x | delta-y | movement-x | movement-y | start-x | start-y)
Requires
core, events
Status
experimental
Category
Events & Actions
Reference
:on(drag)
persistPersistent stateexperimental
Syntax
--x: state(initial, persist("key" [, local | session]))
Requires
core
Status
experimental
Category
Core & State
Reference
persist()
conditionsConditions
Syntax
when(condition, a, b) · :when(condition)
Requires
core
Status
stable
Category
Conditions & Selectors
Reference
when():when()
nth-var:nth-var()
Syntax
:nth-var(--x)
Requires
core
Status
stable
Category
Conditions & Selectors
Reference
:nth-var()
historyHistory
Syntax
previous(--x) · direction(--x) · :changed(--x)
Requires
core
Status
stable
Category
History
formsForm bindings
Syntax
--x: state(initial, bind(value | checked)) · bind: --x [closest(selector)] (a control bound to an ancestor's state) · :just-checked · :just-unchecked
Requires
core
Status
stable
Category
Forms & DOM Bindings
textDOM textexperimental
Syntax
content: var(--x) | when(…) on elements (DOM text) · legacy: text: <value>
Requires
core
Status
experimental
Category
Forms & DOM Bindings
Reference
contenttext:
attributesHTML attributesexperimental
Syntax
--attr-<attribute>: <value> · --x: state(initial, bind(attribute)) · legacy: attr: <attribute> <value>
Requires
core
Status
experimental
Category
Forms & DOM Bindings
Reference
--attr-*attr:
focusDeclarative focusexperimental
Syntax
focus: when(condition) | var(--boolean) | true | false
Requires
core, events
Useful with
forms (never added automatically)
Status
experimental
Category
Focus & Accessibility
Reference
focus:
blur:blurexperimental
Syntax
:blur (from a focus loss until the next focus)
Requires
core
Status
experimental
Category
Focus & Accessibility
Reference
:blur
valuesRandom & typed values
Syntax
random(min, max [, by step]) · pick(a, b, …) · --x: typed(value [, "<syntax>"])
Requires
core
Status
stable
Category
Values & Strings
dynamicLive & pointer values
Syntax
live(expr, interval) · on(event, expr) · mouse(x|y [, local]) · viewport(width|height) · visible([threshold])
Requires
core
Status
stable
Category
Geometry & Observers
geometryElement sizeexperimental
Syntax
element(width) · element(height)
Requires
core
Status
experimental
Category
Geometry & Observers
Reference
element()
  • A counter needs events for its :on(click) rules and text for content: var(--count), over the core.
  • Focus requires events. focus: moves focus only after a trusted user action, which the event dispatcher records. Form bindings record it too, so forms is useful with focus but not required. focus: when(--open) also needs conditions for when(), which the analyzer adds; focus: var(--open) does not.
  • Gestures require events. Drag rules reuse the event dispatcher and the event-time evaluator. gestures is the only feature with pointer-gesture code, so form and event bundles never carry drag.
  • Persistence stands alone. persist needs neither forms nor events: a persisted state works in a core-only page.
  • Forms do not need drag or focus, and blur needs only the core.
  • History as values vs operands. previous(--x) and direction(--x) as operands of a condition, such as :when(direction(--n) > 0), are part of the core. The history feature adds them as values, as in content: previous(--n), and adds :changed().
  • Profiles are named selections, not separate builds: basic, forms, stateful-ui, interactive, dynamic and full. interactive is new in 0.9.0: events, gestures, persistence, conditions, attributes and text.
Full feature tablereactive-css features --json
IDFeatureProvidesRequires
coreCore & state--x: state(initial) · attr(name [, fallback]) / pick(…) / split(text, "d") in state() and set() · previous(--x) / direction(--x) as condition operands · ReactiveCSS.start/stop/refresh/stats/compile/describe/diagnosticsalways included
eventsEvents & actions.x:on(event [, prevent()] [, stop()]) { --state: set(expr) | toggle() | increment(n) | decrement(n) | cycle(a, b) } · :on(keydown(Key)) / :on(outside(click)) · event(field), calc(), min(), max(), clamp(), round(), var(), mouse(), element(), viewport(), random() at event time · legacy: on: trigger(action, …), key(), outside(), prevent()core
gestures
experimental
Drag gestures:on(drag) / :on(drag(start | move | end | cancel)) · event(x | y | delta-x | delta-y | movement-x | movement-y | start-x | start-y)Events & actions
persist
experimental
Persistent state--x: state(initial, persist("key" [, local | session]))core
conditionsConditionswhen(condition, a, b) · :when(condition)core
nth-var:nth-var():nth-var(--x)core
historyHistoryprevious(--x) · direction(--x) · :changed(--x)core
formsForm bindings--x: state(initial, bind(value | checked)) · bind: --x [closest(selector)] (a control bound to an ancestor's state) · :just-checked · :just-uncheckedcore
text
experimental
DOM textcontent: var(--x) | when(…) on elements (DOM text) · legacy: text: <value>core
attributes
experimental
HTML attributes--attr-<attribute>: <value> · --x: state(initial, bind(attribute)) · legacy: attr: <attribute> <value>core
focus
experimental
Declarative focusfocus: when(condition) | var(--boolean) | true | falseEvents & actions
optional: Form bindings
blur
experimental
:blur:blur (from a focus loss until the next focus)core
valuesRandom & typed valuesrandom(min, max [, by step]) · pick(a, b, …) · --x: typed(value [, "<syntax>"])core
dynamicLive & pointer valueslive(expr, interval) · on(event, expr) · mouse(x|y [, local]) · viewport(width|height) · visible([threshold])core
geometry
experimental
Element sizeelement(width) · element(height)core

Missing something? You’ll know

A custom bundle still recognizes all Reactive CSS syntax. When a stylesheet uses a feature the bundle does not contain, the browser ignores that declaration, as it would without the library, and the library adds one entry to ReactiveCSS.diagnostics.

  1. The stylesheet uses.count output { content: var(--n); }
  2. The bundle containscore, events, conditions, focus
  3. ResultThe native content stays, no DOM text is written, and one diagnostic names the feature

A minified bundle reports the diagnostic as a code with its values:

ReactiveCSS.diagnostics
inline <style> #1: RCSS-P001⟦content: var(…) as DOM text⟧⟦text⟧⟦text⟧

Decode it offline with the CLI, or load the development bundle reactive-css.js, which logs the full text:

npx -p @reactivecss/core reactive-css explain 'inline <style> #1: RCSS-P001⟦content: var(…) as DOM text⟧⟦text⟧⟦text⟧'
inline <style> #1: content: var(…) as DOM text is not included in this bundle (feature "text"); build a bundle with it (reactive-css build --features text) or use the full bundle

Fix it by adding the feature and building again:

terminal
npx -p @reactivecss/core -p esbuild reactive-css build --features events,conditions,focus,text --out public/reactive-css.min.js
  • random() and element() also exist in native CSS, so for them the entry is a note rather than a warning.
  • ReactiveCSS.describe().bundle lists the features a bundle contains.
  • After switching a site to a custom bundle, open its pages and check ReactiveCSS.diagnostics.

Analyzer

Let your CSS tell you what it needs.

The analyzer compiles stylesheets with the library’s real compiler and lists the features they use. It handles nested functions, @media, @layer, comments, strings, and native look-alikes such as element(#id), which it does not count. With --build it builds the recommended bundle in the same step.

terminal
npx -p @reactivecss/core -p esbuild reactive-css analyze public/ --build public/reactive-css.min.js

When something is missing from its input, it says so and recommends the full runtime (the warning names the file by its absolute path):

imp/styles.cssreactive
@import url("theme.css");.a { --n: state(0); }.a:on(click) { --n: increment(1); }
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →
npx -p @reactivecss/core -p esbuild reactive-css analyze imp/styles.css
Stylesheets analyzed: 1Features used: eventsRecommended: the full runtime (analysis incomplete (see notes)).warning [import-not-followed] …/imp/styles.css: @import rules are not followed: analyze the imported files too.

With --json, each file also lists its 0.8 declarations (on:, text:, attr:) under legacy, with their 0.9 form. See Migrating from 0.8.

To try it on your own CSS without installing anything, paste it into the Bundle Builder. It runs the same compiler in the page, without sending your CSS anywhere.

Caution · Static analysis sees only what you give it

It cannot see:

  • stylesheets loaded or created by scripts;
  • CSS produced at runtime, or inline style attributes;
  • stylesheets you did not include, including ones loaded with @import.

When the CLI meets a remote stylesheet, an @import, an unreadable file or a page with other scripts, it reports the analysis as incomplete and recommends the full runtime. That fallback is the safe choice whenever you are not sure every stylesheet was included.

Bundle sizes

Smaller by design. Measured in bytes.

v0.9.0 · measured · bytes
Measurement
  • Corecore only45,367
  • Basicbasic profile75,750
  • Full customevery feature, built with the CLI127,105
  • Standard fullreactive-css.min.js149,177

Only the builds in the release report have exact sizes. Any other combination gets its size when you build it: the CLI prints it and writes it to the manifest. Per-feature numbers do not add up, because features share code with the core and with each other. The core is the largest part: it holds the whole compiler, which recognizes every syntax so that missing features can be reported, plus the stylesheet rewrite, the state engine and the scheduler.

Detailed benchmark datadocs/release-0.9.0-hardening.md · benchmarks/audit/results/v090

The library’s benchmark builds each bundle and compresses it with gzip level 9 and brotli quality 11, for the version shown in the report.

BundleFeaturesmingzipbrotli
core onlycore45,36716,90815,362
basiccore, events75,75026,05123,529
formscore, events, forms, text, attributes94,04331,35728,130
stateful-uicore, events, conditions, history, attributes, focus97,53732,92629,301
interactivecore, events, gestures, persist, conditions, text, attributes99,26933,45429,822
dynamiccore, values, dynamic, geometry57,33020,87618,939
fullcore, events, gestures, persist, conditions, nth-var, history, forms, text, attributes, focus, blur, values, dynamic, geometry127,10542,17537,242
reactive-css.min.js (standard)every feature (prebuilt)149,17749,27843,307
reactive-css.compact.min.jsevery feature (prebuilt)128,63342,53037,611

What each feature added when it was built alone over its requirements:

FeatureMeasured overmingzipbrotli
eventscore+30,383+9,143+8,167
gesturescore, events+2,481+824+746
persistcore+3,645+1,295+1,162
conditionscore+7,086+2,432+2,148
nth-varcore+3,934+1,391+1,198
historycore+4,992+1,684+1,524
formscore+8,139+2,553+2,290
textcore+2,853+911+816
attributescore+9,036+2,785+2,394
focuscore, events+3,194+1,109+960
blurcore+957+344+285
valuescore+5,190+1,873+1,652
dynamiccore+4,737+1,508+1,361
geometrycore+2,067+804+687

Advanced configuration

Reference for the build tools.

CLI commands and optionsfeatures · resolve · build · analyze
CommandOptions
features--json
resolve--features a,b, --profile name, --exclude a,b, --json
build--features a,b or --profile name, --exclude a,b, --format iife (default) or esm, --no-minify, --out file, --json
analyzestylesheets, HTML files or folders, --root dir (the folder served as /, for links like /css/site.css), --assume-complete, --build file, --json (adds the legacy syntax of each file)
explaina production diagnostic in quotes, such as "RCSS-P001⟦…⟧"; works offline, without esbuild
  • Exit codes: 0 on success, 1 for input the CLI rejects (unknown feature or profile, a conflict, nothing to build), 2 for an internal error or missing esbuild.
  • Formats. An iife bundle loads with a script tag and starts itself, like the full bundle. An esm bundle exports the same API as the package; call start() yourself.
  • The core alone builds with --features core.
  • The Bundle Builder writes these commands, and the Node script below, for any selection.

Errors name a code and suggest a fix:

npx -p @reactivecss/core -p esbuild reactive-css build --features evnts
error [unknown-feature]: Unknown feature "evnts". Did you mean "events"? Features: core, events, gestures, persist, conditions, nth-var, history, forms, text, attributes, focus, blur, values, dynamic, geometry.
Node API@reactivecss/core/bundle

The same build from a Node script, through the package’s bundle export. It writes a file byte-identical to the CLI’s.

build-reactive-css.mjs
import { build } from '@reactivecss/core/bundle';const result = await build({ features: ['events', 'conditions', 'focus'], outfile: 'public/reactive-css.min.js' });if (!result.ok) throw new Error(result.errors.map((e) => e.message).join('\n'));

build(), resolve(), analyze(), catalog() and sizes() return plain JSON. Invalid input comes back as errors with a code and a message, never as an exception.

Dependency resolutionrequirements, optional features, profiles
  • Every bundle contains the core. Selecting a feature adds what it requires, recursively.
  • A feature listed as optional, such as forms for focus, is reported as “Optional, not included” and never added.
  • --exclude names features that must never be included. If a selected feature requires an excluded one, or a feature is both requested and excluded, the CLI rejects the input as a conflict:
npx -p @reactivecss/core -p esbuild reactive-css build --features focus --exclude events
error [conflict]: "events" is excluded, but "focus" requires it. Remove "events" from the exclusions or "focus" from the features.
  • Profiles are named selections, not separate builds: basic, forms, stateful-ui, interactive, dynamic and full.
Manifestwhat a build records

A build writes a manifest next to the bundle, for example public/reactive-css.min.manifest.json. It records the version, the format, the features and why each one is there, and the sizes, so you can tell which build is deployed. From the build above, without its provides and modules lists:

reactive-css.min.manifest.json
{  "name": "reactive-css",  "version": "0.9.0",  "format": "iife",  "minify": true,  "features": ["core", "events", "conditions", "focus"],  "requested": ["events", "conditions", "focus"],  "added": [],  "excluded": ["gestures", "persist", "nth-var", "history", "forms", "text", "attributes", "blur", "values", "dynamic", "geometry"],  "optionalMissing": [{ "id": "forms", "usefulFor": "focus" }],  "sizes": { "minified": 85540, "gzip": 29370, "brotli": 26343 }}
Security and determinismCSP, runtime, reproducible builds
  • A custom bundle works under a strict Content-Security-Policy, like the full bundle.
  • It needs nothing at runtime. A feature that is not in the bundle is never fetched.
  • Builds are deterministic: equal selections give byte-identical bundles.
  • The website does not build, host or download bundles: building on demand would need a build service, which does not exist. The CLI produces the same file on your machine.

Hosting

Host it yourself.

npm package
The full and compact bundles and the CLI, from the package.
CDN
The full and compact bundles, version-pinned, from jsDelivr or unpkg.
Custom bundle
Built on your machine and served from your site. It has no CDN URL.
index.html
<link rel="stylesheet" href="/styles.css"><script src="/reactive-css.min.js"></script>
  • Rebuild it when you upgrade the package. Bundles from different versions are not interchangeable.
  • Rebuild it when your stylesheets start using a new feature, or run the analyzer in your build step.

The distributions and the CDN status are in the Installation guide.

FAQ

Do I need a custom bundle for a small project?
No. Start with the full bundle. It needs no tooling. Build a custom one when its size matters.
What happens when I start using a new feature?
The declaration is ignored and a diagnostic names the feature to add. Build again with it, or run the analyzer in your build step.
Can the analyzer see dynamically loaded stylesheets?
No. It reads only the files you give it. When it suspects missing input, it reports the analysis as incomplete.
Do custom bundles work with a strict CSP?
Yes, like the full bundle. Serve the file from your own origin.
Full, compact or custom?
Full when you are not sure what your stylesheets use. Compact is the same full runtime with a shorter describe() registry; it is not a feature selection. Custom when you know, and the analysis is complete.
Can I load a custom bundle from a CDN?
No. Only the full and compact bundles will be published. A custom bundle is a file you host.
Experimental · syntax may change between minor versions