Custom Bundles
guidenewShip 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.
reactive-css analyzereactive-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.
- Your CSSYou pass the stylesheets and pages you serve. The analyzer reads only those inputs.
- Feature analyzer
reactive-css analyzecompiles them with the library’s own compiler and lists the Reactive CSS features they use. Native CSS is left alone. - Dependency resolverThe core is always included. Each feature adds what it requires:
focusaddsevents. - Custom bundle
reactive-css.min.js, one file you host yourself, and a manifest that records the version, features and sizes.
For this stylesheet:
.tabs { --tab: state(1); }.tab:on(click) { --tab: set(2); }.panel:when(--tab == 2) { display: block; }the analyzer reports:
Stylesheets analyzed: 1Features used: events, conditionsRecommended: --features events,conditionsQuick start
Your first custom bundle.
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 tonpx reactive-css: without a local install, that runs an unrelated npm package of that name.terminalnpm install --save-dev @reactivecss/core@0.9.0 esbuildAnalyze 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.
terminalnpx -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.
Build
Build the bundle. The CLI prints what it included and why, the size, and the path of the manifest.
terminalnpx -p @reactivecss/core -p esbuild reactive-css build --features events,conditions,focus --out public/reactive-css.min.jsoutputBuilt 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.jsonUse the file
Serve the file from your site and load it with a script tag. An
iifebundle 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.
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 & statealways
- 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 & actionsrequires core
- 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 gesturesexperimentalrequires events
- 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 stateexperimentalrequires core
- Syntax
- --x: state(initial, persist("key" [, local | session]))
- Requires
- core
- Status
- experimental
- Category
- Core & State
- Reference
- persist()
conditionsConditionsrequires core
- Syntax
- when(condition, a, b) · :when(condition)
- Requires
- core
- Status
- stable
- Category
- Conditions & Selectors
nth-var:nth-var()requires core
- Syntax
- :nth-var(--x)
- Requires
- core
- Status
- stable
- Category
- Conditions & Selectors
- Reference
- :nth-var()
historyHistoryrequires core
- Syntax
- previous(--x) · direction(--x) · :changed(--x)
- Requires
- core
- Status
- stable
- Category
- History
- Reference
- previous()direction():changed()
formsForm bindingsrequires core
- 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 textexperimentalrequires core
- Syntax
- content: var(--x) | when(…) on elements (DOM text) · legacy: text: <value>
- Requires
- core
- Status
- experimental
- Category
- Forms & DOM Bindings
attributesHTML attributesexperimentalrequires core
- Syntax
- --attr-<attribute>: <value> · --x: state(initial, bind(attribute)) · legacy: attr: <attribute> <value>
- Requires
- core
- Status
- experimental
- Category
- Forms & DOM Bindings
focusDeclarative focusexperimentalrequires events
- 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:blurexperimentalrequires core
- Syntax
- :blur (from a focus loss until the next focus)
- Requires
- core
- Status
- experimental
- Category
- Focus & Accessibility
- Reference
- :blur
valuesRandom & typed valuesrequires core
- Syntax
- random(min, max [, by step]) · pick(a, b, …) · --x: typed(value [, "<syntax>"])
- Requires
- core
- Status
- stable
- Category
- Values & Strings
dynamicLive & pointer valuesrequires core
- Syntax
- live(expr, interval) · on(event, expr) · mouse(x|y [, local]) · viewport(width|height) · visible([threshold])
- Requires
- core
- Status
- stable
- Category
- Geometry & Observers
- Reference
- live()on()mouse()visible()viewport()
geometryElement sizeexperimentalrequires core
- Syntax
- element(width) · element(height)
- Requires
- core
- Status
- experimental
- Category
- Geometry & Observers
- Reference
- element()
- A counter needs
eventsfor its:on(click)rules andtextforcontent: 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, soformsis useful withfocusbut not required.focus: when(--open)also needsconditionsforwhen(), which the analyzer adds;focus: var(--open)does not. - Gestures require events. Drag rules reuse the event dispatcher and the event-time evaluator.
gesturesis the only feature with pointer-gesture code, so form and event bundles never carry drag. - Persistence stands alone.
persistneeds neitherformsnorevents: a persisted state works in a core-only page. - Forms do not need drag or focus, and
blurneeds only the core. - History as values vs operands.
previous(--x)anddirection(--x)as operands of a condition, such as:when(direction(--n) > 0), are part of the core. Thehistoryfeature adds them as values, as incontent: previous(--n), and adds:changed(). - Profiles are named selections, not separate builds:
basic,forms,stateful-ui,interactive,dynamicandfull.interactiveis new in 0.9.0: events, gestures, persistence, conditions, attributes and text.
Full feature tablereactive-css features --json
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.
- The stylesheet uses
.count output { content: var(--n); } - The bundle contains
core,events,conditions,focus - ResultThe native
contentstays, no DOM text is written, and one diagnostic names the feature
A minified bundle reports the diagnostic as a code with its values:
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:
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 bundleFix it by adding the feature and building again:
npx -p @reactivecss/core -p esbuild reactive-css build --features events,conditions,focus,text --out public/reactive-css.min.jsrandom()andelement()also exist in native CSS, so for them the entry is a note rather than a warning.ReactiveCSS.describe().bundlelists 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.
npx -p @reactivecss/core -p esbuild reactive-css analyze public/ --build public/reactive-css.min.jsWhen something is missing from its input, it says so and recommends the full runtime (the warning names the file by its absolute path):
@import url("theme.css");.a { --n: state(0); }.a:on(click) { --n: increment(1); }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.
It cannot see:
- stylesheets loaded or created by scripts;
- CSS produced at runtime, or inline
styleattributes; - 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.
- Corecore only45,36716,90815,362
- Basicbasic profile75,75026,05123,529
- Full customevery feature, built with the CLI127,10542,17537,242
- Standard fullreactive-css.min.js149,17749,27843,307
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.
What each feature added when it was built alone over its requirements:
Advanced configuration
Reference for the build tools.
CLI commands and optionsfeatures · resolve · build · analyze
- Exit codes:
0on success,1for input the CLI rejects (unknown feature or profile, a conflict, nothing to build),2for an internal error or missing esbuild. - Formats. An
iifebundle loads with a script tag and starts itself, like the full bundle. Anesmbundle exports the same API as the package; callstart()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:
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.
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
formsforfocus, is reported as “Optional, not included” and never added. --excludenames 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 aconflict:
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,dynamicandfull.
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:
{ "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.
<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.