Changing scoped CSS variables using JavaScript

· ITNEXT ·

4 min read Original article ↗

Useful for theming

Tobias Uhlig

I googled for this topic quite a bit and did not find a reasonable solution, so I thought it might be worth a short article.

Content

  1. The problem
  2. How to change CSS vars inside :root
  3. How to change scoped CSS variables
  4. Changing scoped CSS vars from within a Web Worker
  5. Creating a shortcut
  6. The neo.mjs project
  7. Off topic, but incredible: What does ChatGPT think about neo?

1. The problem

In case you are working with multiple themes, it feels like a common use case to not put your CSS variables directly under :root .

Instead, you might use a structure like
:root .theme-light {/*my variables*/}

In case you want to change the values of these CSS vars at run-time using JavaScript, this can be a bit tricky.

2. How to change CSS vars inside :root

:root {
--my-color: red;
}
document.documentElement.setProperty('--my-color', 'blue');

No-brainer :)

3. How to change scoped CSS variables

:root .theme-light {
--my-color: red;
}

:root .theme-dark {
--my-color: darkred;
}

I am using this structure inside the neo.mjs frontend framework. The nice part about it is that we can apply a theme to any level of the DOM, which enables us to nest themes.

We can e.g. apply theme-light to the viewport, use a child container with theme-dark and inside of that one buttons with theme-light again.

I was hoping for an elegant way to change CSS variables for this use case too. I did not find it. So:

/**
* @param {Object} data
* @param {String} data.key
* @param {String} [data.priority] optionally pass 'important'
* @param {String} data.theme
* @param {String} data.value
*/
function setCssVariable(data) {
let key = data.key,
rule, sheet;

if (!key.startsWith('--')) {
key = '--' + key;
}

for (sheet of document.styleSheets) {
if (sheet.href.includes(data.theme)) {
for (rule of sheet.cssRules) {
if (rule.constructor.name === 'CSSStyleRule') {
if (rule.style.getPropertyValue(key) !== '') {
rule.style.setProperty(key, data.value, data.priority);
return true;
}
}
}
}
}

return false;
}

setCssVariable({
key : 'my-color',
theme: 'theme-light',
value: 'blue'
});

This algorithm feels a bit expensive, but it still executes reasonably fast.

We are iterating over all stylesheets of the document. Assuming that your CSS file URL contains the name of your theme, you can limit the amounts of checks by it.

In case it does not, you can check for the selectorText inside the inner loop (more expensive):

Press enter or click to view image in full size

For the neo use case, each theme file has just one CSSStyleRule, so the inner loop is not as expensive as it might look at the first glance.

Get Tobias Uhlig’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

Non existing property values return an empty string (interesting :)), so we can check for it to identify which CSSRule contains the variable that we do want to change.

Accessing the rule.style property again gives us:

and we are done.

You can use this technique inside your Angular, React or whatever mainstream tech you are going with app. The next sections are beyond this.

4. Changing scoped CSS vars from within a Web Worker

src/main/addon/Stylesheet.mjs (main thread)

class Stylesheet extends Base {
static getConfig() {return {
className: 'Neo.main.addon.Stylesheet',

remote: {
app: [
'setCssVariable'
]
}
}}

setCssVariable(data) {
// ...
}
}

For the neo scope this part is trivial. We can just expose the main thread addon method to the app worker and we are done.

Press enter or click to view image in full size

If you look close at the console you will notice the blue box → the scope is set to a Web Worker and we can just call the method as a promise within the same otherwise empty namespace.

5. Creating a shortcut

src/worker/App.mjs (off the main thread)

class App extends Base {
construct(config) {
super.construct(config);

// convenience shortcut
Neo.setCssVariable = this.setCssVariable.bind(this);
}

setCssVariable(data) {
let addon = Neo.main?.addon?.Stylesheet,
theme = Neo.config.themes?.[0];

if (!addon) {
return Promise.reject('Neo.main.addon.Stylesheet not imported');
} else {
if (theme.startsWith('neo-')) {
theme = theme.substring(4);
}

return addon.setCssVariable({theme, ...data});
}
}
}

And now we can just call:

Press enter or click to view image in full size

from within a Web Worker.

6. The neo.mjs project

“An application worker being the main actor” or “off the main thread” are no longer fiction, but work very stable and extremely fast:

neo has already hit v4.8.2 with more than 15.000 commits inside the ecosystem. You might have missed thousands since my last blog post.

In case you have not looked into the benefits of performance, scalability and extensibility, you might want to catch up with the next gen multithreaded frontends topic.

As a spoiler: Max Rahder is just in the middle of teaching the first professional neo training for 20 students. Once finished, we should have enough content to finally launch the missing learning section.

In case you are interested to get an early access, just ping us on Slack:

7. Off topic, but incredible: What does ChatGPT think about neo?

Press enter or click to view image in full size

Press enter or click to view image in full size

Best regards & happy coding,
Tobias