CSS-based state management
CSS custom properties are incredibly powerful and versatile.
- You can store basically anything in them (yes, anything!).
- They are automatically inherited by the entire DOM sub-tree.
- JavaScript can read and write their values.
Combine all that with native events, and you get a JavaScript state management solution that uses CSS as the source of truth.
// use state
const state = () => getComputedStyle(element).getPropertyValue("--state");
// send updates from higher up in the tree
document.body.style.setProperty("--state", "new value!");
document.body.dispatchEvent(new Event("--state", { bubbles: true }));
// listen to updates down the tree
element.addEventListener("--state", () => {
console.log(state());
}, { capture: true });
It doesn’t always have to be stored all the way up in <body>
either. You can store it in a common ancestor of all child nodes that need access. It’s kinda like context if you’re used to that pattern.
I made a small counter example recently as a proof-of-concept.
But why?
CSS-based state management is useful when state needs to be shared by multiple independent components.
This independency may be due to multiple unrelated libraries or multiple versions or instances of the same library. Context fails surprisingly often in complex codebases with transient dependencies.
There are ways to get around the context problem by decoupling state from components, e.g. using signals or stores. And that will generally be the best thing to do in most cases.
For styling needs (think theming), you might be able to get away with using classes or data attributes, but it starts to get real unwieldy when you have nested containers that might contain any arbitrary components.
The strength of this technique lies in its inheritence capabilities. You can easily provide some state to all descendant DOM nodes, while restricting access to ancestors. At any level, you can also override the state for a particular sub-tree. These kinds of things are quite difficult to do with just JavaScript.
Some examples
Lets say you want to give your users the ability to toggle underlines from all links, except the ones in the header or navigation.
<body style="--underline: yes">
<header style="--underline: no">
<!-- These links will not be underlined -->
</header>
<!-- All other links will be underlined -->
</div>
Or maybe you want to allow changing the density of a table between spacious and compact (similar to Gmail).
<body>
<table style="--density: compact">
<!-- All components inside the table will use reduced spacing -->
</table>
<!-- Everything else unaffected -->
</body>
The inline style is mostly a convenience choice. You could even define these inside your CSS files, for one or more elements at the same time. One caveat is that you would need to do some more trickery if you want to “watch” for changes.
section {
--mode: accordion;
@media (min-width: 50em) {
--mode: tabs;
}
}
If you do use an inline style, you gain the ability to emulate higher-order custom properties by writing what I call “style attribute queries”. Probably don’t overuse this though, because attribute selectors can hurt performance.
[style*="--density: compact"] {
--scale: 0.75;
--hit-target: 1.5rem;
}
All the examples I’m thinking of are primarily concerned with styling. The data is small and infrequently changing, and can be set from a central place. Compare this to something like a class or data attribute, which would need to be set over and over again on every element that uses it.
The future
Admittedly, this is all a bit of a hack. What I would much rather use is style queries (currently only in Chrome), so I can skip the JavaScript wherever possible.
Until that happens though (and for obscure JavaScript-y things), I can still benefit from the other powerful aspects of custom properties. Maybe you can too. Just don’t let the Tailwind crowd see you.