141

Alright, I'm creating a system for my webpage that allows users to change the theme. How I want to accomplish this is by having all the colors as variables, and the colors are set in the :root part of the CSS.

What I want to do is change those colors via JavaScript. I looked up how to do it, but nothing that I attempted actually worked properly. Here's my current code:

CSS:

:root {
  --main-color: #317EEB;
  --hover-color: #2764BA;
  --body-color: #E0E0E0;
  --box-color: white;
}

JS:

(Code to set the theme, it's ran on the click of a button) - I didn't bother adding the :root change to the other 2 themes since it doesn't work on the Dark theme

function setTheme(theme) {
  if (theme == 'Dark') {
    localStorage.setItem('panelTheme', theme);
    $('#current-theme').text(theme);
    $(':root').css('--main-color', '#000000');
  }
  if (theme == 'Blue') {
    localStorage.setItem('panelTheme',  'Blue');
    $('#current-theme').text('Blue');
    alert("Blue");
  }
  if (theme == 'Green') {
    localStorage.setItem('panelTheme', 'Green');
    $('#current-theme').text('Green');
    alert("Green");
  }
}

(Code that is ran when the html is loaded)

function loadTheme() {
  //Add this to body onload, gets the current theme. If panelTheme is empty, defaults to blue.
  if (localStorage.getItem('panelTheme') == '') {
    setTheme('Blue');
  } else {
    setTheme(localStorage.getItem('panelTheme'));
    $('#current-theme').text(localStorage.getItem('panelTheme'));
  }
}

It shows the alert, but does not actually change anything. Can someone point me in the right direction?

7
  • Is your js in run when the document is ready? How do you determine that it 'does not change anything'? Change your example to something closer to a minimal reproducible example Commented Jun 14, 2016 at 2:20
  • Updated the OP with more code, sorry. Commented Jun 14, 2016 at 2:24
  • It's still not obvious what you expect to change. for instance, on your console you can type, say, $(':root').css("background-color", "#000000") and see things change. So if something visual change is not taking place, the problem is likely not your code (unless it's running before document ready) Commented Jun 14, 2016 at 2:31
  • I want to change the color of the variables with JS. For instance, in my example I'm attempting to change --main-color from #317EEB to #000000. Commented Jun 14, 2016 at 2:34
  • Open up the console. Run the statement that sets the color. Run a statement that queries the color. See if you get what you set. Commented Jun 14, 2016 at 2:35

14 Answers 14

266

Thank you @pvg for providing the link. I had to stare at it for a little to understand what was going on, but I finally figured it out.

The magical line I was looking for was this:

document.documentElement.style.setProperty('--your-variable', '#YOURCOLOR');

That did exactly what I wanted it to do, thank you very much!

Sign up to request clarification or add additional context in comments.

11 Comments

ironically it looks like getPropertyValue is not woking with :root css variables, but setting does
The way to get the value is: getComputedStyle(element).getPropertyValue("--my-var");
i like using document.querySelector(":root") instead of document.documentElement, as i am sure this is the same element as :root { ... } in css
document.querySelector(':root') === document.documentElement returns true - however, TypeScript complains about the former one; I doubt there's a good reason for it complaining though :/
@MaxWaterman Well with documentElement, you are going straight to the element which we know for certain is the HTMLElement. querySelector returns the first Element it finds matching your query. Element is the base class - many classes such as SVGElement and HTMLElement inherit from it. Because we are passing it a string (crossing the line of how smart TS can be), it can't be sure that you will get an HTMLElement although for this single case... we know it will be HTMLElement. You could use something that guarantees an HTMLElement: document.getElementsByTagName('html')[0].
|
33

I think this is cleaner and easier to remember. to set/get css variables to/from :root

const root = document.querySelector(':root');

// set css variable
root.style.setProperty('--my-color', 'blue');

// to get css variable from :root
const color = getComputedStyle(root).getPropertyValue('--my-color'); // blue

Example: setting multiple variables all at once

const setVariables = vars => Object.entries(vars).forEach(v => root.style.setProperty(v[0], v[1]));
const myVariables = {
  '--color-primary-50': '#eff6ff',
  '--color-primary-100': '#dbeafe',
  '--color-primary-200': '#bfdbfe',
  '--color-primary-300': '#93c5fd',
  '--color-primary-400': '#60a5fa',
  '--color-primary-500': '#3b82f6',
  '--color-primary-600': '#2563eb',
  '--color-primary-700': '#1d4ed8',
  '--color-primary-800': '#1e40af',
  '--color-primary-900': '#1e3a8a',
};
setVariables(myVariables);

2 Comments

That is not "at once" - that is just codding an iterator on the properties of an object!
@horiatu, it is how underlying CSSStyleDeclaration interface works - you set properties one by one. It is actually correct idiomatic way to add properties
26

For those who want to modify the actual style sheet the following works:

var sheet = document.styleSheets[0];
sheet.insertRule(":root{--blue:#4444FF}");

More info at here: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule

4 Comments

i just discovered that "setProperty" does something similar, maybe. so i am wondering if this would work too? document.body.style.setProperty('--blue', '#4444ff'); what do you think?
@edwardsmarkf that way is changing the DOM whereas the code above is changing the style sheet itself. Changing the stylesheet allows for themes to change on the fly without reloading. All elements using the --blue variable immediately change when the color value is changed. With the DOM you have to change each element you want to update.
thanks - i "discovered" this after i made the comment, otherwise i probably should have removed it. i have learned as well that using the insertRule might conflict with a previous rule, and perhaps the original rule should be removed? It's strange that we cannot easily add new rule to existing rule without some jScript acrobatics like appending new rule after the initial semicolon.
@edwardsmarkf Daedalus's solution also updates the theme without reloading, so this seems superior since you can overwrite existing properties - which avoids those acrobatics.
8

To use the values of custom properties in JavaScript, it is just like standard properties.

// get variable from inline style
element.style.getPropertyValue("--my-variable");

// get variable from wherever
getComputedStyle(element).getPropertyValue("--my-variable");

// set variable on inline style
element.style.setProperty("--my-variable", 4);

1 Comment

The first example doesn't work for :root css properties - it only works for values that have been set directly with setProperty. Need to use getComputedStyle(element).getPropertyValue(......)
8

I came here looking how to toggle the :root color-scheme with JavaScript, which sets the browser to dark mode (including the scroll bars) like this:

:root {
    color-scheme: dark;
}

using the @Daedalus answer above, this is how I implemented my dark mode detection from user preference:

    const userPrefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const preferredTheme = userPrefersDarkMode ? 'dark' : 'light';
    document.documentElement.style.setProperty("color-scheme", preferredTheme);

or with saved toggle:

    const savedTheme = localStorage.getItem('theme');
    if (savedTheme == 'dark') {
        thisTheme = 'light'
    }
    else {
        thisTheme = 'dark'; // the default when never saved is dark
    }
    document.documentElement.style.setProperty("color-scheme", thisTheme);
    localStorage.setItem('theme', thisTheme);

see also the optional meta tag in the header:

<meta name="color-scheme" content="dark light">

Comments

6

TL;DR

A solution to the problem could be the below code:

const headTag = document.getElementsByTagName('head')[0];
const styleTag = document.createElement("style");

styleTag.innerHTML = `
:root {
    --main-color: #317EEB;
    --hover-color: #2764BA;
    --body-color: #E0E0E0;
    --box-color: white;
}
`;
headTag.appendChild(styleTag);

Explanation:

Although @Daedalus answer with document.documentElement does the job pretty well, a slightly better approach is to add the styling into a <style> HTML tag (solution proposed).

If you add document.documentElement.style then all the CSS variables are added into the html tag and they are not hidden:

CSS style inline to html

On the other hand, with the proposed code:

const headTag = document.getElementsByTagName('head')[0];
const styleTag = document.createElement("style");

styleTag.innerHTML = `
:root {
    --main-color: #317EEB;
    --hover-color: #2764BA;
    --body-color: #E0E0E0;
    --box-color: white;
}
`;
headTag.appendChild(styleTag);

the HTML tag will be cleaner and if you inspect the HTML tag you can also see the :root styling as well.

HTML tag without styles

1 Comment

Good point on how it looks upon inspection.
2

old jquery magic still working too

$('#yourStyleTagId').html(':root {' +
    '--your-var: #COLOR;' +
'}');

Comments

1

We can use the registerProperty() using the @property CSS at-rule.

window.CSS.registerProperty({
  name: "--my-color",
  syntax: "<color>",
  inherits: false,
  initialValue: "#c0ffee",
});

Comments

0

Read only, retrieve all CSS --root rules in an array, without using .getComputedStyle().

This may allow to retrieve values before full DOM content load, to create modules that use global root theme variables, but not via CSS. (canvas context...)

/* Retrieve all --root CSS variables
*  rules into an array
*  Without using getComputedStyle (read string only)
*  On this example only the first style-sheet
*  of the document is parsed
*/
console.log(

  [...document.styleSheets[0].rules]
   .map(a => a.cssText.split(" ")[0] === ":root" ?
    a.cssText.split("{")[1].split("}")[0].split("--") : null)
   .filter(a => a !== null)[0]
   .map(a => "--"+a)
   .slice(1)

)
:root {
    --gold: hsl(48,100%,50%);
    --gold-lighter: hsl(48,22%,30%);
    --gold-darker: hsl(45,100%,47%);
    --silver: hsl(210,6%,72%);
    --silver-lighter: hsl(0,0%,26%);
    --silver-darker: hsl(210,3%,61%);
    --bronze: hsl(28,38%,67%);
    --bronze-lighter: hsl(28,13%,27%);
    --bronze-darker: hsl(28,31%,52%);
}

Comments

0

As per the answer of DEV Tiago França, which worked on my case. here is why it works like a charm:

/* Root original style */
:root {
    --hl-color-green: green;
}
/* Using * overrides the variable for all html tags where it is actually used */
* {
    --hl-color-green: #0cc120;
}

Comments

0

If you want to target the stylesheet you can do this:

    const styleSheet = document.styleSheets[0]

    const ruleIndex = [...styleSheet.cssRules].findIndex(rule => rule.cssText.includes('--sidebar:'))
    const ruleText = styleSheet.cssRules[ruleIndex].cssText
    let newRule: string

    if (isOpen()) newRule = ruleText.replace('--sidebar: var(--sidebar-open)', '--sidebar: var(--sidebar-closed)')
    else newRule = ruleText.replace('--sidebar: var(--sidebar-closed)', '--sidebar: var(--sidebar-open)')

    styleSheet.deleteRule(ruleIndex)
    styleSheet.insertRule(newRule, ruleIndex)

Just be sure that the string to replace is exactly the same as in your stylesheet, including spaces;

CSS:

:root {
    --sidebar: var(--sidebar-open);
    --sidebar-open: 15rem;
    --sidebar-closed: 2rem;
}

Text to teplace:

Works:

--sidebar: var(--sidebar-open)

Doesn't works (One space is missing):

--sidebar:var(--sidebar-open)

The answer from @Michael works, but it adds a new rule every time you switch the style.

Comments

0
private switchDarkMode = (value: boolean) => {
    let darkStyles = document.getElementById("darkStyles");
    if (!darkStyles) {
        darkStyles = domConstruct.create(
            "style",
            {
                id: "darkStyles",
            },
            document.head
        );
    }
    darkStyles.innerHTML = `
:root {
    --white: ${!value ? "white" : "black"};
};
`;
    }

Comments

0

there is a good example of this on this tutorial

To simplify the tutorial, we could do the following

In the body you could have something like this:

:root {
  --main-color: #42b983;
}

body {
  background-color: var(--main-color);
}

To access and change the variable in javascript, you could do the following thing:

// Get the root element
const root = document.documentElement;

// Retrieve the current value of a CSS variable
const currentColor = getComputedStyle(root).getPropertyValue('--main-color');

// Update the CSS variable
root.style.setProperty('--main-color', '#ff5733');

I am not thackling the whole javascript interactions here, as it already has been done in the ansers before, I am just showing the code that updates the root color.

Comments

-3
/*My style*/
:root {
    --hl-color-green: green;
}

/*My update*/
* {
 --hl-color-green: #0cc120;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.