Lightweight, on-demand CSS engine. It reads your class names directly from the DOM, builds scoped CSS rules, and injects them into the page.
The runtime scans elements you explicitly mark, parses their class names, and generates exactly the CSS you need — nothing more. It supports responsive breakpoints, dark mode, group states, CSS cascade layers, a safelist for dynamic classes, and a full API for programmatic control.
This page covers every runtime feature, including several that aren’t documented elsewhere.
Mark any element with data-fs to include it in the runtime scan:
The runtime queries [data-fs], parses every class on each match, and generates scoped CSS. Without data-fs, classes are invisible to the runtime — no error, no CSS, no effect.
Only the second element produces CSS. The first is left entirely untouched.
Only the inner div is scanned. The outer .card is a plain layout container — the runtime doesn’t touch it.
Append any of these suffixes to a base class name to generate a state-scoped rule:
| State | CSS produced |
|---|---|
:hover | .class:hover { … } |
:active | .class:active { … } |
:focus | .class:focus { … } |
:focus-within | .class:focus-within { … } |
:target | .class:target { … } |
:checked | .class:checked { … } |
:disabled | .class:disabled { … } |
:group-hover | .group:hover .class { … } |
opacity:hover, bg:focus, color:group-hover — the pattern is always base:state.
Prefix any class with a breakpoint name to wrap it in a @media (min-width: …) block:
| Prefix | Min-width |
|---|---|
sm: | 40rem |
md: | 48rem |
lg: | 64rem |
xl: | 80rem |
2xl: | 96rem |
Breakpoint rules are placed inside the same CSS layer as their base rule, just wrapped in the appropriate media query.
Prefix with dark: to scope a rule to a .dark ancestor:
Dark mode can be combined with responsive prefixes and states:
The generated selector becomes .dark .lg\:dark\:opacity\:hover — applied only when .dark wraps the element and the viewport is at least 64rem.
The safelist generates CSS for classes that aren’t present in the DOM at scan time — essential for dynamically injected elements, server-rendered HTML, or classes set via JavaScript after load.
Set FS.safelist before the runtime initializes:
Each string is a full class name, parsed exactly like a DOM class:
Use the object form to generate every combination of states, breakpoint prefixes, and dark/light variants from a single base class:
| Property | Type | Description |
|---|---|---|
class | string | Base class name (e.g. opacity) |
states | string[] | States to generate (e.g. [':hover', ':focus']) |
prefixes | string[] | Breakpoint prefixes (e.g. ['md', 'lg']). Omit for no prefix. |
dark | boolean | If true, also generates all dark-mode variants |
The object form builds the full matrix: every prefix × every state × dark and light.
Returns the parsed list of class objects that will be included in the next style generation pass.
FS.customRules lets you extend or replace built-in rule definitions. Each rule maps a base class name to one or more CSS properties and their values.
Set FS.customRules before the runtime initializes:
If your selector matches an existing built-in rule, your entry replaces it entirely.
| Field | Required | Description |
|---|---|---|
selector | Yes | Base class name this rule matches (e.g. opacity) |
properties | Yes | CSS property or array of properties |
values | Yes (unless arbitrary) | Values mapped to each property |
arbitrary | No | If true, value is read from a CSS variable — no values needed |
placeholders | No | Template substitution: parts of values replaced by CSS vars |
layer | No | Cascade layer: components, styles (default), or utilities |
When arbitrary: true, the CSS value is always a variable derived from the class name, state, prefix, and dark flag — you define it per element in style:
Variable naming convention: --[dark-][prefix-]baseClass-state.
Placeholders let you template values where specific parts are replaced by CSS variables:
Each key in placeholders is a substring in values that gets replaced by the computed CSS variable name for the given state.
Returns the complete merged rule set (built-ins + your custom rules) that the runtime is currently using.
Enable debug mode during development to surface silent failures:
Debug scans every [class] and [cls] element in the document and runs two checks:
data-fs — warns if an element has interactive state classes but no data-fs attribute, meaning those classes will silently do nothing.Each warning includes the count of missing items, the variable names, and the actual DOM node so you can jump straight to it in DevTools.
State classes without data-fs:
Incorrect variable name (camelCase vs. kebab-case):
Partial state coverage — one variable present, one missing:
cls attribute is also validated:
FS.debug is validation-only. CSS generation still requires data-fs.
| Scope | Generates CSS | |
|---|---|---|
data-fs | That element only | Yes |
FS.debug | Entire document | No |
The runtime exposes these methods on the global FS object after initialization.
FS.refresh()Clears the rule cache and re-runs style generation from scratch. Use this after dynamically adding data-fs elements or changing FS.safelist at runtime:
FS.regenerate()Re-runs style generation without clearing the cache. Faster than refresh() when the rule definitions haven’t changed:
FS.getCache()Returns the internal Map of generated rules, keyed by a string combining class + state + dark + prefix:
Useful for inspecting what the runtime has already compiled, or checking whether a specific class has been processed.
FS.getRules()Returns the complete merged rule array — built-ins plus any custom rules — currently active:
FS.getSafelist()Returns the parsed safelist entries that will be included in the next generation pass:
All generated CSS lives inside @layer blocks. The layer is controlled per rule via the layer field (default: styles):
This means component styles always yield to utility overrides, and utilities always win — without !important. Use the layer field in FS.customRules to place your rules in the right tier.
| Operation | DOM query |
|---|---|
| Style generation | document.querySelectorAll('[data-fs]') |
| Debug validation | document.querySelectorAll('[class], [cls]') |
The runtime is fast by default because it only touches elements you’ve opted in. Debug mode scans the entire document and should be disabled in production.
data-fs to any element using an interactive state class.FS.debug = true before shipping.data-fsNo CSS is generated. No error is thrown. The class silently does nothing — exactly what makes this hard to spot. Enable debug mode; it will warn you.
The runtime derives variable names by replacing separators with hyphens. camelCase will never match. Always use --base-state form.
Debug validates. It never generates. You still need data-fs on the element for CSS output.
If you inject data-fs elements after the page has loaded, the runtime won’t see them until you explicitly trigger a refresh: