Cover image for Reactive CSS Explained

Reactive CSS Explained

adam_cyclones profile image Adam Crockett Updated on ・4 min read

Edit, I have released the library reactive-css-properties and anyone wanting to use this library from npm should follow those instructions, most notably, no need for async await

Some of you will remember my popular series on JavaScript enhanced SCSS by setting and getting css variables in JavaScript rather than targeting DOM nodes directly, also this was indirectly controlled from css calling JavaScript using css variables, honestly it's a mad idea which many seemed to love, its my most popular post by about 5000 or so.

Anyways ever since then I have been working on my own stuff using this method, but it got me thinking along more sane lines... if I change a css variable value I want this to be reactive, eg change a variable in devtools and actually respond from JavaScript.

I have tried several times to write this unicorn function and most of the time it failed until today that is!

Ladies and gentleman I give you unnamed library (see the console bellow, oh, ps its WIP and wrote in Typescript but don't let that put you off, it will work in JavaScript)

let's break it down a bit, here's the usage:

import cssProps from "./cssProps";

const { fontFamily } = cssProps();

(async () => {
  const result = await fontFamily("blue");
  result.onChange((detail) => {
    console.log("e", detail);
  await fontFamily("blue");
  await fontFamily("yellow");

Let's look at the first interesting thing.

const { fontFamily, myVar } = cssProps();

At first glance this might look kind of normal so I added something else to be desteuctured, both properties of this function's return object should not exist, they should be undefined, and yet... not only do they exist, simply by getting them, they specify the CSS variable name we will soon create. This is a smoke and mirrors trick I devised whilst hacking with JavaScript Proxy. You can see the similarities with React hooks style but mine's not made of arrays mwhaha!
I wanted to design the interface with the bear minimum boilerplate to get to work with variables pretty quickly, it doesn't even look to out of the ordinary.

What else to know about this line?
You can pass an element to become the root scope of your css variables, by default it's document.documentElement but for custom elements where the root is :host you can just pass this instead, or even some DOM node to affect only its dependents.

What next? Oh yes:

(async () => {

Because mutations to the DOM can happen at any async moment, I choose to use an observer, this is all native 0 dependencies (currently). We watch the root node for the style attribute to change. So yeah it's async, and because top level await is not a thing we have wrap around an async function which calls immediately, I rue the day I can not do this.

const result = await fontFamily("blue");

So our destructured property by any name... its callable! Once called, you have then pass a CSS value, in this case it's "blue", you can see this --font-family:blue; in the style attribute of this example <html style='--font-family:blue;'>. In the words of the Cat In The Hat, "that is not all, oh no, that is not all!" something else just happened, we just emitted a custom event css-prop-change (I am not entirely happy with this yet, I may change to an observable and bring in RXJS).

What is the value of result then?

    key: "--font-family",
    onChange: Ζ’ value(cb),
    value: "blue"

Its this 100% immutable object, (remind me to .freeze() it 🀫), you could do this:

// some css in js
    background: ${result.key} 

Obviously result is a bad name but you can see the point, its trivial to insert a usage of this variable key. Or in-fact just write the name in a style sheet in anticipation of your amazing variable to be declared.

What else can you do?

// after you set your first variable (initial state if you like)
result.onChange((detail) => {
  console.log("e", detail);

You now have access to onChange but I may refactor this too subscribe like I said, events.. not sure about it.

Any callback you provide to onChange will then be fired on... change, and the callback is passed a detail about what just changed, this comes from the event listener we have pre-added automatically. Something like this:

    event: CustomEvent {isTrusted: false, detail: {…}, type: "css-prop-change", target: html, currentTarget: null, …}
    newValue: "yellow"
    oldValue: "blue"

But of course, this only fires if the value actually changes, so setting something to blue twice will do nothing, "YAY!".

The main and most amazing thing is, if a variable that was set and tracked by our function changes externally, IN REAL TIME, we see callback fire too, you can literally go to the devtools window and change the value and see the callback fire in response, you could even change the variable through breakpoints @media providing the variable is pre declared in css already and this will also fire on break points, (personally I prefer matchMedia API).

Anyway what do you think, should I release it?

(ps, If your wondering, I am taking a break from didi before I burn out - so much to do, need more contrib)

Posted on by:

adam_cyclones profile

Adam Crockett


I work at ForgeRock as Staff UI Engineer, I play with all sorts really. Lately WASM is my toy of interest.


markdown guide

Version 1.1.0 is being worked on and it's going to be better because:

  • no async or promise needed
  • no custom event
  • CSS custom property fallback will be supported

Polymorphism means that themeTextColor can be called(), address.someMethod(), oh and its a string too:


import rCSSProps from "./cssProps";

const { themeTextColor } = rCSSProps();

// watch for changes
themeTextColor.subscribe((change) => {
  console.log("Im watching for theme text changes", change);

// set the variable and inject into the dom

setTimeout(() => {
  // change the theme text color after 3 seconds to simulate dark mode toggled
}, 3000);

// just stringify it... more magic!
  background: ${themeTextColor};

I have exhausted every magic trick I know in JavaScript to make this easy to use I will be publishing this thing once I have added a fallback. The code above is in a forked sandbox, I will do another writeup.


Dude, so epic. Nice post.


Thanks querty, I took this one seriously :P