@uistate/css
v2.0.1
Published
State-driven CSS: runtime reactive styling, design tokens, typed validation, and relational constraints
Downloads
202
Maintainers
Readme
@uistate/css
State-driven CSS: runtime reactive styling, design tokens, typed validation, and relational constraints. Powered by @uistate/core.
Install
npm install @uistate/css @uistate/coreStyle Engine
Compiles EventState dot-paths into real CSS rules. Path segments become selectors, pseudo-classes, and CSS properties automatically.
import { createEventState } from '@uistate/core';
import { createStyleEngine } from '@uistate/css';
const store = createEventState();
const engine = createStyleEngine(store);
// Paths become CSS rules
store.set('css.card.background', '#fff');
store.set('css.card.padding', '1.5rem');
store.set('css.card.hover.boxShadow', '0 4px 6px rgba(0,0,0,0.1)');
// -> .card { background: #fff; padding: 1.5rem; }
// -> .card:hover { box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
// Subtree set — swap an entire theme in one call
store.set('css.card', {
background: '#1e293b',
color: '#e2e8f0',
hover: { background: '#334155' },
});Design System
Reactive design tokens. Bind tokens to styles: change a token, every bound style updates.
import { createDesignSystem } from '@uistate/css';
const ds = createDesignSystem(store, {
tokens: {
color: { primary: '#3b82f6', danger: '#ef4444', surface: '#fff', text: '#1e293b' },
spacing: { xs: '0.25rem', sm: '0.5rem', md: '1rem', lg: '1.5rem', xl: '2rem' },
radius: { sm: '0.25rem', md: '0.5rem', lg: '1rem', full: '9999px' },
font: { sm: '0.875rem', base: '1rem', lg: '1.25rem', xl: '1.5rem' },
},
});
// Bind style paths to tokens
ds.bind('css.btn.background', 'color.primary');
ds.bind('css.btn.padding', 'spacing.md');
ds.bind('css.btn.borderRadius', 'radius.md');
// Change token -> all bound styles update
ds.setToken('color.primary', '#8b5cf6');
// Bulk theme swap
ds.setTokens({ color: { primary: '#22c55e', surface: '#0f172a', text: '#e2e8f0' } });
// Inspect bindings
ds.getBindings(); // { 'color.primary': ['css.btn.background', ...] }Typed CSS
Runtime schema validation for styles. Define what properties a component accepts and get meaningful errors on invalid values.
import { createTypedCSS } from '@uistate/css';
const typed = createTypedCSS(store, {
btn: {
background: { type: 'color' },
padding: { type: 'length', min: '0.25rem', max: '3rem' },
fontSize: { type: 'length', min: '0.75rem', max: '2rem' },
display: { type: 'enum', values: ['flex', 'inline-flex', 'block', 'none'] },
},
card: {
background: { type: 'color' },
padding: { type: 'length' },
boxShadow: { type: 'shadow', maxLayers: 3 },
},
});
store.set('css.btn.padding', '1rem'); // ✅ valid
store.set('css.btn.padding', '10rem'); // ⚠️ [typed-css] btn.padding: '10rem' exceeds max '3rem'.
store.set('css.btn.display', 'table'); // ⚠️ [typed-css] btn.display: 'table' not allowed.
store.set('css.btn.background', 'banana');// ⚠️ [typed-css] btn.background: not a valid color.
// Add schemas at runtime
typed.defineComponent('badge', { background: { type: 'color' } });
// Inspect schema from state
store.get('schema.btn'); // { background: { type: 'color' }, ... }Relational CSS
Constraint-based style relationships. Proportional scaling, modular type scales, and WCAG contrast enforcement.
import { createRelationalCSS } from '@uistate/css';
const rel = createRelationalCSS(store);
// Proportional: header padding is always 2x card padding
rel.derive('css.header.padding', { ref: 'css.card.padding', multiply: 2 });
// Modular type scale from a base font size
rel.scale('tokens.font.base', {
'css.h1.fontSize': 2.0,
'css.h2.fontSize': 1.5,
'css.h3.fontSize': 1.25,
'css.body.fontSize': 1,
'css.small.fontSize': 0.875,
});
// Change tokens.font.base -> all headings recompute proportionally
// WCAG contrast: auto-pick text color for readability
rel.contrast('css.card.color', {
against: 'css.card.background',
light: '#ffffff',
dark: '#1e293b',
minRatio: 4.5, // AA compliance
});
// Change card background to dark -> text automatically switches to white
// Clamp a value
rel.clamp('css.sidebar.width', { ref: 'css.sidebar.width', min: '200px', max: '400px' });Testing
Two-layer testing architecture:
self-test.js: Zero-dependency self-test (53 assertions). Runs automatically on npm install via postinstall. Tests pure functions: camelToKebab, parsePath, color/length validation, hexToRgb, contrastRatio, parseLength, and formatLength.
node self-test.jstests/css.test.js: Integration tests via @uistate/event-test (26 tests). Tests the eventState-driven modules through the store: designSystem (token binding, propagation, unbind), typedCSS (schema validation, violations, runtime defineComponent), and relationalCSS (derive, scale, contrast, clamp).
npm test| Suite | Assertions | Dependencies |
|-------|-----------|-------------|
| self-test.js | 53 | none (zero-dep) |
| tests/css.test.js | 26 | @uistate/event-test, @uistate/core |
Note:
cssStateandstyleEnginerequire a DOM and are tested in browser. The v2 modules (designSystem,typedCSS,relationalCSS) work entirely through eventState and are fully testable in Node.
Legacy: CSS Custom Properties (v1)
The original CSS state layer: writes state to CSS custom properties and data attributes. Still available for WordPress, static sites, and progressive enhancement.
import { createCssState } from '@uistate/css/cssState';
const ui = createCssState({ theme: 'light' });
ui.init();
ui.setState('theme', 'dark');
// -> :root has --theme: dark and data-theme="dark"License
MIT — see LICENSE.
Copyright © 2025 Ajdin Imsirovic
