@uistate/css 1.0.0
CSS-native state management using CSS custom properties and data attributes. No bundler, no framework — state lives in CSS and is inspectable in DevTools.
Install
npm install @uistate/css
Overview
@uistate/css stores state directly in CSS custom properties (--key: value) on :root and as data-* attributes on elements. This means:
- CSS selectors can react to state:
[data-theme="dark"] { ... } - State is visible in DevTools (Elements → Computed → CSS variables)
- No JavaScript needed after initial state is set — CSS handles the visual response
- Custom properties pierce Shadow DOM boundaries
createCssState(initialState)
Creates a CSS state manager. Call .init() to inject the managed <style> element and apply initial state.
import { createCssState } from '@uistate/css';
const ui = createCssState({ theme: 'light', count: 0 });
ui.init();
// State is now in CSS:
// :root { --theme: light; --count: 0; }
// <html data-theme="light" data-count="0">
setState / getState
ui.setState('theme', 'dark');
// :root now has --theme: dark
// <html> now has data-theme="dark"
const theme = ui.getState('theme'); // 'dark'
Values are serialized to CSS-compatible strings automatically. Objects and arrays are JSON-serialized.
observe(key, callback)
Watch for state changes. Returns an unobserve function.
const unobserve = ui.observe('theme', (value) => {
console.log('Theme changed to:', value);
});
ui.setState('theme', 'dark'); // logs "Theme changed to: dark"
unobserve(); // stop watching
Declarative Binding
Bind state to the DOM without JavaScript using data-observe and data-state-action attributes.
<!-- Auto-updates text content when 'count' changes -->
<span data-observe="count">0</span>
<!-- Sets state on click -->
<button data-state-action="theme" data-state-value="dark">Dark Mode</button>
<button data-state-action="theme" data-state-value="light">Light Mode</button>
Serializer
Configurable serialization between JavaScript values and CSS-compatible strings. Three modes:
| Mode | Description |
|---|---|
'escape' | Escapes special CSS characters. Best for simple strings. |
'json' | Full JSON serialization. Best for objects/arrays. |
'hybrid' | Strings are escaped, objects are JSON. Default mode. |
import { createSerializer } from '@uistate/css';
const ser = createSerializer({ mode: 'hybrid' });
ser.serialize('hello world'); // 'hello\\ world'
ser.serialize({ a: 1 }); // '{"a":1}'
ser.deserialize('hello\\ world'); // 'hello world'
Template Manager
Component mounting via <template> elements, event delegation via data-action attributes, and CSS-variable-based templating.
import { createCssState, createTemplateManager } from '@uistate/css';
const ui = createCssState({ user: 'Alice' });
ui.init();
const tm = createTemplateManager(ui);
// Register an action handler
tm.registerAction('greet', (e) => {
alert(`Hello, ${ui.getState('user')}!`);
});
// Attach event delegation to a container
tm.attachDelegation(document.getElementById('app'));
<div id="app">
<button data-action="greet">Say Hello</button>
</div>
WordPress
Drop a <script> tag into your theme. No build step, no React, no webpack. State lives in CSS custom properties that your existing stylesheets can reference.
<script type="module">
import { createCssState } from 'https://esm.sh/@uistate/css';
const ui = createCssState({ sidebar: 'open', theme: 'light' });
ui.init();
</script>
<style>
[data-sidebar="closed"] .sidebar { display: none; }
[data-theme="dark"] { --bg: #1a1a2e; --text: #eee; }
</style>
Static Sites (Jekyll, Hugo, 11ty)
Add interactivity to server-rendered HTML. The server provides structure, @uistate/css adds reactive state without a SPA framework.
Web Components
CSS custom properties defined on :root cascade into Shadow DOM. This makes @uistate/css a natural theming bridge for web component architectures.
// Inside a web component
connectedCallback() {
// Read the global theme token
const theme = getComputedStyle(this).getPropertyValue('--theme').trim();
this.shadowRoot.innerHTML = `<p>Current theme: ${theme}</p>`;
}