Treehouse

Add Rules to Stylesheets with JavaScript

By on  

Since we're using so much JavaScript in our web applications these days, we're looking for more ways to keep them fast.  We use event delegation to keep event listening efficient, we use function debouncing to limit the number a times a given method can be used, use JavaScript loaders to load only the resources we need, and so on.  Another way we can make our pages efficient and fast is to dynamically add and remove styles directly to a stylesheet instead of constantly querying the DOM for elements and applying styles.  Here's how it works!

Getting the Stylesheet

Which stylesheet you add the rules to is up to you.  If you have a specific stylesheet in mind, you can add an ID to the LINK or STYLE element within your page HTML and get the CSSStyleSheet object by referencing the element's sheet property.  The stylesheets can be found in the document.styleSheets object:

var sheets = document.styleSheets; // returns an Array-like StyleSheetList

/*
Returns:  

StyleSheetList {0: CSSStyleSheet, 1: CSSStyleSheet, 2: CSSStyleSheet, 3: CSSStyleSheet, 4: CSSStyleSheet, 5: CSSStyleSheet, 6: CSSStyleSheet, 7: CSSStyleSheet, 8: CSSStyleSheet, 9: CSSStyleSheet, 10: CSSStyleSheet, 11: CSSStyleSheet, 12: CSSStyleSheet, 13: CSSStyleSheet, 14: CSSStyleSheet, 15: CSSStyleSheet, length: 16, item: function}
*/

// Grab the first sheet, regardless of media
var sheet = document.styleSheets[0];

One important consideration is the media of the stylesheet -- you want to ensure you aren't adding rules to a print stylesheet when you expect the styles to display on screen.  A CSSStyleSheet object does have informational properties for you to peruse:

// Get info about the first stylesheet
console.log(document.styleSheets[0]);

/*
Returns:  

CSSStyleSheet
	cssRules: CSSRuleList
	disabled: false
	href: "http://davidwalsh.name/somesheet.css"
	media: MediaList
	ownerNode: link
	ownerRule: null
	parentStyleSheet: null
	rules: CSSRuleList
	title: null
	type: "text/css"
*/

// Get the media type
console.log(document.styleSheets[0].media.mediaText)
/*
Returns:
	"all" or "print" or whichever media is used for this stylesheet
*/

In any event, there are many ways to grab a stylesheet to attach style rules to.

Creating a New Stylesheet

In many cases, it may just be best to create a new STYLE element for your dynamic rules.  This is quite easy:

var sheet = (function() {
	// Create the <style> tag
	var style = document.createElement("style");

	// Add a media (and/or media query) here if you'd like!
	// style.setAttribute("media", "screen")
	// style.setAttribute("media", "@media only screen and (max-width : 1024px)")

	// WebKit hack :(
	style.appendChild(document.createTextNode(""));

	// Add the <style> element to the page
	document.head.appendChild(style);

	return style.sheet;
})();

Unfortunately WebKit requires a hack to properly get things going but all we care about is having that sheet.

Adding Rules - Nonstandard addRule

CSSStyleSheet objects have an addRule method which allows you to register CSS rules within the stylesheet.  The addRule method accepts three arguments:  the selector, the second the CSS code for the rule, and the third is the zero-based integer index representing the style position (in relation to styles of the same selector):

sheet.addRule("#myList li", "float: left; background: red !important;", 1);

The default for index is -1, which means the end of the collection.  For extra/lazy control, you may add !important to rules to avoid problems with the index.  addRule calls return a result of -1 in all cases -- it really doesn't represent anything.

Remember that the advantage here is that elements added from the page automatically have the styles applied to them; i.e. you wont have to add them to elements as they're injected into the page.  Efficient!

Inserting Rules

Stylesheets also have an insertRule method which isn't available in earlier IE's.  The insertRule combines the first two arguments of addRule:

sheet.insertRule("header { float: left; opacity: 0.8; }", 1);

This method may seem a bit uglier but is useful nonetheless.

Safely Applying Rules

Since browser support for insertRule isn't as global, it's best to create a wrapping function to do the rule application.  Here's a quick and dirty method:

function addCSSRule(sheet, selector, rules, index) {
	if(sheet.insertRule) {
		sheet.insertRule(selector + "{" + rules + "}", index);
	}
	else {
		sheet.addRule(selector, rules, index);
	}
}

// Use it!
addCSSRule(document.styleSheets[0], "header", "float: left");

This utility method should cover all cases of new style application.  If you are nervous about applying variable styles across your app, it's reasonable to wrap the inner code of this method in a try{}catch(e){} block.

Inserting Rules for Media Queries

Media query-specific rules can be added in one of two ways. The first way is through the standard insertRule method:

sheet.insertRule("@media only screen and (max-width : 1140px) { header { display: none; } }");

Of course since IE hasn't always supported insertRule, the other method is creating a STYLE element with the proper media attribute, then adding styles to that new stylesheet. This may require juggling multiple STYLE elements, but that's easy enough. I would probably create an object with media queries as indexes, and create/retrieve them that way.

Dynamically adding rules to stylesheets is efficient and easier than you may think.  Keep this strategy in mind on your next big app, as it may save you work in both code and element processing.

ydkjs-6.png

Recent Features

  • Facebook Open Graph META&nbsp;Tags

    It's no secret that Facebook has become a major traffic driver for all types of websites.  Nowadays even large corporations steer consumers toward their Facebook pages instead of the corporate websites directly.  And of course there are Facebook "Like" and "Recommend" widgets on every website.  One...

  • CSS&nbsp;Gradients

    With CSS border-radius, I showed you how CSS can bridge the gap between design and development by adding rounded corners to elements.  CSS gradients are another step in that direction.  Now that CSS gradients are supported in Internet Explorer 8+, Firefox, Safari, and Chrome,...

Incredible Demos

  • Scrolling &#8220;Agree to Terms&#8221; Component with MooTools&nbsp;ScrollSpy

    Remember the good old days of Windows applications forcing you to scroll down to the bottom of the "terms and conditions" pane, theoretically in an effort ensure that you actually read them? You're saying "No David, don't do it." Too late -- I've done...

  • MooTools TextOverlap&nbsp;Plugin

    Developers everywhere seem to be looking for different ways to make use of JavaScript libraries. Some creations are extremely practical, others aren't. This one may be more on the "aren't" side but used correctly, my TextOverlap plugin could add another interesting design element...

Discussion

  1. Wow, could make it easier to parse custom css on the client. :)

    Are there any reasons to why you didnt’t add addCSSRule to the CSSStyleSheet prototype? That way you could skip the first argument and add the rule to the CSS directly. Or if you just added it to “sheet”.

    • Not a horrible idea but then I’d get a barrage of hate for modifying prototypes ;)

  2. Would you prefer this approach rather than lazy loading a stylesheet by inserting it in the DOM for special use cases? I’m wondering it it’s creating some penalties other than reflows.

  3. This would definitely cut down on DOM queries! Javascript is not my strongest suit. Your blog is very helpful.

  4. Fred

    Tips:

    Inserting multiple statement at once:

    e.g. you need to insert this rules:
    “selector1 { rule : value } selector2 { rule : value }”

    Use this:

    sheet.insertRule(“@media all { selector1 { rule : value } selector2 { rule : value } }”, 0);

    otherwise you will get parsing error.

  5. MaxArt

    I swear, dealing with style sheet rules is *a pain*!
    There has never been consensus on the APIs in the past, essentially because the W3C never stated anything about it. So, as usualy, IE had a different set of APIs, but even among “standard” browsers like Firefox and Opera there were some differences on the way the rules are dealt, grouped and whatever.

    Hopefully we’ll see something more consistent as IE8 usage becomes meaningless.

  6. Fred

    Tips:

    In the case of older IE, you can use old technique of inserting CSS on the runtime which has been around for a very long time. Check out this article:

    http://yuiblog.com/blog/2007/06/07/style/

  7. @Mikael Söderström Adding properties to native objects is considered a bad (and dangerous) practice. A better approach would be creating a new object extending CSSStyleSheet (http://perfectionkills.com/extending-built-in-native-objects-evil-or-not/). The only cases I would think of extending a built-in directly are shims.

    Stylesheets have bigger impact on loading times, than they used before, but I don’t think it’s necessary to be so careful about them unless we are developing with some low-spec mobile devices in mind (I would consider optimizing the css selector structure and reduce number of DOM element at first). However, being able to create css rules this way makes creating and editing css @keyframes* based animations a lot easier (and looks cleaner as well).

    * which are a lot more efficient than solutions using requestAnimationFrame() or setInterval(), also we still will have to wait for web animations.

  8. Bryan Forbes

    You should check out the interface dgrid has implemented for doing this (which also adds the ability to easily remove added rules and old IE support):

    https://github.com/SitePen/dgrid/blob/master/util/misc.js#L84

    dgrid uses dynamic stylesheets for quite a bit including column resizing.

  9. It’s worth pointing out that this APIs are defined in the CSSOM spec:

    http://dev.w3.org/csswg/cssom/

  10. would you please elaborate it why we use it instead of css stylesheet.

    • You still use a CSS stylesheet; I’m saying you add rules to that stylesheet instead of directly applying styles to nodes.

  11. Thanks for the article David. I have found this a useful technique, especially in prototyping. I wrapped this in a library call Csster [http://github.com/ndp/csster]. BTW I ended up moving away from adding individual rules and simply making the browser do all the parsing by inserting a STYLE block… it was just simpler.

  12. Publius

    Typo: a hack to property

  13. Good article, thanks for another winner David.

  14. Leon de Rijke

    Great explanation, thanks. How would you add @-rules, like @font-face or @keyframes?

  15. Hmm. An interesting approach but I’m not sure I see what the advantage is.

    Your styles are now spread throughout your JS and are those addStyle calls really any less expensive than adding a class to the DOM?

  16. Tony

    Excellent article

  17. Hooman

    That’s absolutely great but there is a question how can we remove rules from a selector?

    • Excellent question — I guess you just overwrite the rule (last one wins)? Again and again and again?

  18. A few comments:

    1. I’m a little skeptical of the claim that this is efficient, though that really depends on what you’re comparing it to. Dynamically adding rules to a style sheet leads to rerunning selector matching on the entire document, at least in Gecko. Maybe faster than running multiple querySelectorAll calls after anything changes, but that doesn’t mean it’s fast.

    2. The addRule method you describe is not standard and not cross-browser. insertRule is standard. See http://dev.w3.org/csswg/cssom/#cssstylesheet

  19. Yaron Shapira

    Firefox (I have v25) does not have a addRule method for CSSStyleSheet. It instead has a insertRule method (which also exists in Chrome (I tested v31) and IE11. The syntax is different – it’s a full rule string like “body { background: red; }”

  20. And what about fontFaces? What’s the correct variant?

  21. pike

    I had various issues with ie<9 with the above code. But I found that adding css specs to a new node just worked fine. With jQuery: $('' + css + '').appendTo('head');

    Of course, that doesn’t allow you to modify the rules later.

  22. pike

    oops – code got mangled in the above comment. I meant:

    $('[style type="text/css"]' + css + '[/style]').appendTo('head'); 
    

    without the square brackets.

  23. Tim

    I just wanted to say thank you. You helped me run my script 8-9 times as fast as before.

  24. Ok so my code was riddled with mistakes :) 100% correct version, tested on this page and turned the text red:

    // get a stylesheet
    var styles = document.styleSheets[0];
    
    // we insert an empty rule just to create a new CSSStyleRule object
    // the second param is the index to insert at
    // using the length property we effectively "append" the rule to the end of the sheet
    var ruleIndex = styles.insertRule("p {}", styles.cssRules.length)
    
    // the index is the position the rule was inserted at, so we can now get a handle on it
    var rule = styles.cssRules.item(ruleIndex);
    
    // now we have it, modify it's styles!
    // applied to the document immediately
    rule.style.color = "red";
    
  25. Some years ago I got all excited about the performance implications of modifying the CSS rules and leaning on the browser’s internal machinery rather than doing lots of DOM queries and setting style properties in js. And in some cases it does turn out to be faster – table column widths was my canonical example. But when I went to write this up and did more cross-browser/platform testing to confirm my findings I found the results wildly variable – probably due to the issues dbaron points out: “Dynamically adding rules to a style sheet leads to rerunning selector matching on the entire document, at least in Gecko”

    The upshot wasnt nearly as exciting as I hoped: it’s sometimes faster in some specific cases, in particular environments. YMMV and measure don’t guess.

    That said, as far as patterns go, making presentation changes across a document by changing the stylesheet seems preferable to querying and manipulating style properties on the elements themselves. Maybe this rerunning selector matching on the entire document issue should be filed as a bug? It seems heavy-handed and like we could do a bit better. And a rudimentary selector API for CSSOM (find rules efficiently) wouldn’t hurt too.

  26. Hi David

    Thanks for this article, I love the ideas in it.

    I noticed that when I tried using the code in this article, the Webkit hack was causing issues in IE but removing it didn’t seem to cause any issues. Can you remember which versions of Webkit were affected and what happens if the hack isn’t included?

    Cheers
    Skateside

  27. A little improvement to the original addCSSRule function: Basically, it does not add additional cssRules for the same selector, instead it appends the rules to the existing one, and it returns the resulting cssRule.

      var addCSSRule = function(sheet, selector, rules){
        //Backward searching of the selector matching cssRules
        var index=sheet.cssRules.length-1;
        for(var i=index; i>0; i--){
          var current_style = sheet.cssRules[i];
          if(current_style.selectorText === selector){
            //Append the new rules to the current content of the cssRule;
            rules=current_style.style.cssText + rules;
            sheet.deleteRule(i);
            index=i;
          }
        }
        if(sheet.insertRule){
          sheet.insertRule(selector + "{" + rules + "}", index);
        }
        else{
          sheet.addRule(selector, rules, index);
        }
        return sheet.cssRules[index].cssText;
      }
    

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!