by

CSSOM revisited: Modifying styles with JavaScript a more sane way

Reading Time: 3 minutes

Wait, what’s the CSSOM? Well, I’ve talked about it in the past; it’s the object model for your styles. I even wrote about how to make a CSSOM analyzer.

What I didn’t know then, that I know now, is that you can manipulate the CSSOM. It’s not just there for show and tell. You can add styles and stylesheets. In fact, it might be a pretty useful way to manipulate styles with JavaScript (if that’s your thing).

If you want to learn more about the CSSOM, Jake Rocheleau over at Hongkiat wrote a pretty good introduction  that I’d encourage you to read.

The big discovery

As I’ve mentioned before, stylesheets are objects accessible in document.stylesheets. I originally thought you couldn’t manipulate those stylesheets, but as it turns out, you can! I just didn’t know the right way to manipulate them.

What do I mean by “manipulate”? That you can add and remove style rules. What that means is that libraries like Aphrodite or CSSX are possibly doing it wrong. They should be talking to the CSSOM, not the DOM.

Adding and manipulating styles through the CSSOM

Here are the steps for adding a stylesheet and a rule in the CSSOM (and bypassing the need to actually add text in the <style>)

  1. We should create a <style>
  2. We add it to the DOM
  3. We get the stylesheet from the CSSOM
  4. We add styles to the stylesheet

Let’s Code!

Let’s start with an object called JSCSS:

JSCSS = {} || JSCSS;

Create a Stylesheet and Add it to the DOM

Fun fact: every time you create a <style>, that adds a stylesheet to the CSSOM. So let’s create a method that does that.

I’m using ES6 (ECMAScript 6/ Harmony), so this syntax may look a little funky. I’ll explain in my code

// use ES6' default parameters to create a default title for the stylesheet; you can always pass in a parameter of your choice

JSCSS.addStyleSheet = function (title = `jscss-${Date.now()}`) {
    const styleSheet = document.createElement('style');
    
    styleSheet.title = title;
    document.body.append(styleSheet); // Now the stylesheet is part of the CSSOM
    
}

Get the stylesheet

We’ll need to make a function that can get a stylesheet. So let’s add one to our JSCSS object:

JSCSS.getStyleSheet = function (title) {
    let styleSheet;
    
    for (let i = 0; i < document.styleSheets.length; i++) {
        if (document.styleSheets[i].title === title) styleSheet = document.styleSheets[i];
    }
    return styleSheet;
};

Now that we can get a stylesheet, we should modify the addStyleSheet method so that it can actually return it for us:

JSCSS.addStyleSheet = function (title = `jscss-${Date.now()}`) {
    const styleSheet = document.createElement('style');
    
    styleSheet.title = title;
    document.body.append(styleSheet);
    
    const CSSStyleSheet =  this.getStyleSheet(title);
    
    return CSSStyleSheet;
}

Sweet, so now, we can run var myStyleSheet = JSCSS.addStyleSheet(); and get a Stylesheet object back.

Add styles to the stylesheet

We have one tiny little problem so far. A CSSStyleSheet only gives you insertRule(), not an insertRules(). That kinda sucks. So let’s fix that in two steps:

  1. Make a function that can split our rulesets into an array and insert each one by one
  2. Add that function directly to the CSSStyleSheet we created.

Maybe there’s a smarter/better/faster way, but for right now, you can split your rulesets into an array by splitting on the }.

JSCSS._insertRules = function (rules) {
    let rulesArray = rules.split('}');
    
    rulesArray.forEach((rule)=> {
          if (rule.indexOf('{') !== -1) this.insertRule(rule, 0);  //insertRule actually requires a position within the CSSStylesheet
    });
}

But, this is our function! That this is bound to the JSCSS object. We want this this to be the CSSStyleSheet object. Let’s go back to addStyleSheet one more time:

JSCSS.addStyleSheet = function (title = `jscss-${Date.now()}`) {
    const styleSheet = document.createElement('style');
    
    styleSheet.title = title;
    document.body.append(styleSheet);
    
    const CSSStyleSheet =  this.getStyleSheet(title);
    CSSStyleSheet.insertRules = this._insertRules.bind(CSSStyleSheet); // gives our newly created stylesheet an insertRules()
    
    return CSSStyleSheet;
}

How would you use this?

Now that we’ve done this, we can run the following test:

const MyStyleSheet = JSCSS.addStyleSheet();
const styleRule1 = `body {font-family: Comic sans; font-size: 2em;}`;
const styleRules = `
    h2 {
       color: #933;
    }
    h3 {
       font-size: .75em;
    };
`; // using the new template literal for easy multiline use
MyStyleSheet.insertRule(styleRule1, 0);
MyStyleSheet.insertRules(styleRules,0);

You’ll find that your styles have changed, but that the <style> that you created has no text in it. Some might consider this a good thing, others may not.

Wrap-up

First, lemme get this out of the way: I am not endorsing styles managed with JavaScript. Separate your concerns. It’s just better that way.

But if you do need to manipulate styles with JavaScript, maybe it’s easier than we think…

I’ve got a small demo over on CodePen where I added some additional features (not many are needed, the CSSOM is pretty robust).

check it out:

See the Pen JSCSS by Paceaux (@paceaux) on CodePen.

July 27, 2020 update: I’ve now made this a proper ES6 class as a gist