Add Rules to Stylesheets with JavaScript

By  on  

Update:Rewrites made to accurately represent current practice for stylesheet injection.

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: "https://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", "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.

Inserting Rules

Stylesheets have an insertRule method which isn't available in earlier IE's but is now the standard for rule injection.  The insertRule requires that you write the entire CSS rule just as you would in a stylesheet:

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

This method may seem a bit ugly for a JavaScript API but that's how it works. The second argument, the index, represents the index at which to insert the rule. This is helpful so that you can insert the same rule/code and define which wins out. 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.

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);

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!

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("insertRule" in sheet) {
		sheet.insertRule(selector + "{" + rules + "}", index);
	}
	else if("addRule" in sheet) {
		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.

Recent Features

  • By
    Page Visibility API

    One event that's always been lacking within the document is a signal for when the user is looking at a given tab, or another tab. When does the user switch off our site to look at something else? When do they come back?

  • By
    CSS 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

  • By
    jQuery Countdown Plugin

    You've probably been to sites like RapidShare and MegaUpload that allow you to download files but make you wait a specified number of seconds before giving you the download link. I've created a similar script but my script allows you to animate the CSS font-size...

  • By
    Create a NoScript Compatible Select Form Element with an onChange Event

    I wouldn't say that I'm addicted to checking Google Analytics but I do check my statistics often. I guess hoping for a huge burst of traffic from some unknown source. Anyway, I have multiple sites set up within my account. The way to...

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 ;)

    • Deryck

      This was a great catch I definitely love saving keystrokes.

      Thanks

  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;
      }
    
  28. John Madhavan-Reese

    This line in your Creating a New Stylesheet example is incorrect:

    // style.setAttribute("media", "@media only screen and (max-width : 1024px)")

    It should omit the ‘@media ‘ like the commented line before it does.

  29. You should check out veinjs – a little script I wrote to do something very similar:
    https://github.com/israelidanny/veinjs

  30. One thing worth noting about dealing with adding rules to the stylesheet: Prefixed properties may need special handling depending on your use-case.

    If you add a rule with a property which uses a prefix like -webkit, -moz or whatever, all the non-supported ones will get stripped out. So for example, you could add a -moz prefixed property in Webkit, and it will simply strip it out.

    Thus, if you actually want to for example save the CSS you’ve generated for whatever reason, you’ll need to use some other approach to include the prefixed versions for other browsers.

  31. Good point!
    But when I test the performance in the Chrome timeline, I found that when using insertStyle/deleteStyle, it makes the Recalculate Style time more longer than change style directly on the DOM or change the classes, about 1ms to 0.3ms respectively in my simple test.

    And I look inside what’s going on when the browser is recalculating the style, I found that when I edit the style rule in the styleSheet, it forces the browser to recaculate all the style rules, and the worst thing is this performance gap is growing when my CSS rule is getting more complex.

    If there’s some image that is base64 format, the browser need to parse all that images again, which makes it more slower. In my another test, which I have lots base64 images, the results of Recaculate Style time becomes 5ms to 0.3ms respectively.

    • Danny Povolotski

      Hey, check this out:
      http://jsperf.com/inject-vs-apply/8

      I came up with a little library of my own that injects CSS into a style element , and I’m seeing amazing performance.

  32. Great article, it’s helped me crack the back of a persistent problem I’ve had on a big web app build.

    For my usage I needed to clear all the CSS rules each time I was going to update them so I decided to make a function to handle this. It might come in handy for anyone who wants to delete / remove rules.

    function clearCSSRules(sheet){
      
      var i = sheet.cssRules.length - 1 ;
      
      // Remove all the rules from the end inwards.
      while(i >= 0){
        
        if("deleteRule" in sheet) { sheet.deleteRule(i); }
        else if("removeRule" in sheet) { sheet.removeRule(i); }
        
        i--;
        
      }
      
      
    }
    
  33. Hey David, is there an event to listen about when the cssRules are parsed?

  34. Kevin

    The latest version of IE(11.0.9600) still doesn’t support the insertRule() method on windows 7, use appendRule() instead!

    • John Madhavan-Reese

      I’m using sheet.insertRule in IE 11.0.9600.17420 on Windows 7 Ultimate 64-bit. Works just fine.

  35. Nvc

    Safari 8 and FF 34 don’t update styles with Webkit hack. Probably this hack is outdated. (Chrome 39 works well with and without this line.)

  36. You can use wjs library for that. Once installed, use :

    wjs.use('CssLink', 'yourcss.css');
  37. Patrick

    I think it’s worth saying that dynamically changing stylesheets this way may lead to some devtools weirdness.
    For instance, in chrome, open a page that runs JS to update its own stylesheet. Then open the chromedevtools and select an element that has a CSS rule that was just changed.
    You won’t see the changes in the Styles tab, instead you’ll see the unaltered, as-authored style.

    The thing is it’s pretty important for browser devtools to show the styles as they have been authored. And dynamic updates to the CSS object-model aren’t easily reconciled with the original as-authored stylesheet.

    Right now, firefoxdevtools does *not* show the styles as they were authored, so this scenario will work.

  38. But why not use the Modernizr-approach by setting a class on html or body to set various things to how you want them. It seems a lot simpler to me and doesn’t require you to do any css hacking in the first place.

    And i also like to suggest to use normal mediaqueries in the first place. If your content doesn’t do that right than you might think about doing these dirty solutions but i still don’t see why you would ever need any of this. It just seems like your classes are all wrong

  39. That is a very great idea. But what about browser support David.

  40. I wrote a small JS Perf, and as I was suspecting this is extremely unlikely to perform in any situation, manipulating the DOM directly is still the best thing to do (or even better just swap CSS classes)
    http://jsperf.com/stylesheet-vs-dom-manipulation

  41. Havelly

    Brilliance and simplicity go hand-in-hand. You need to write a book.

  42. Inspired by this article, I’ve made this lib: https://github.com/cssobj/cssobj

    Manipulate CSSOM and CSS Rules totally from JS

    
    var obj = { div: {
      position: 'fixed',
      zIndex: '102',
      display: 'none',
      top: '50%',
      left: '50%'
    }}
    
    // create cssom from JS object
    var result = cssobj(obj)
    
    // update rule later
    obj.div.display = 'block'
    result.update()
    
    // add rule
    obj.div.p = { color:'red' }
    result.update()
    
    // remove rule
    delete obj.div.p
    result.update()
    
    

    Also can see the demo page: https://github.com/cssobj/cssobj

  43. oliver

    In what situations would adding a new stylesheet be better than manipulating classes with JS?

    Mozilla says: “Add a stylesheet rule to the document (may be better practice, however,
    * to dynamically change classes, so style information can be kept in
    * genuine stylesheets (and avoid adding extra elements to the DOM))”

  44. Susan

    Using JS, how would I change what is inside an existing css class?

  45. re “Stylesheets have an insertRule method which isn’t available in earlier IE’s but is now the standard for rule injection”

    …you might be missing the fact that MSIE -6-7-8-… had styleSheet instead of sheet and there’s little documentation ‘out there’ to connect the two

    …it appears my (old) MSIE6-8 JScript styleobj.styleSheet.rules[0].style should be updated to styleobj.sheet.rules[0].style (very simple after hours of researching the Internet)

  46. I thought I would add some context to the discussion. I can think of two reasons why you need to style something using JavaScript rather than just doing the job in the Style Sheet and possibly using JavaScript to toggle things:

    First, there may be some values which cannot be determined in a static style sheet. For example, you may set a colour dependent on the time of day, or some dimensions dependent on a loaded image. JavaScript is well suited to setting dynamic values.

    Second, you may be writing a JavaScript widget which adds one or more elements to the page. You might then use JavaScript to complete the job and set suitable styles for the added elements.

    This article offers a simple technique which is much cleaner that hacking the .style property.

  47. Ilya Gromov

    Is there any way to save the changes that were made to the stylesheet?
    I want to allow users to modify the styles and then save the changes made. Stylesheet is an external css file that I’m generating on the server so I need to send updated styles back either as plain css or an object that I can parse into css on the server

  48. Thanks for this article, David. It’s helped me get past some very annoying limitations in a website builder I’m working with that doesn’t allow me to add custom CSS in any “normal” way. I’m going to be able to make a number of small tweaks using this method. However, I’m hitting a limitation with media queries. I’ve read the above about how they need to be handled differently, but I’m not a developer and don’t know enough about JavaScript to get it to work. Any suggestions on how to add in a simple media query per the below example?

    The following works to make the site logo larger:

    
    /* Credit: davidwalsh.name */
    /* specify our style rules in a string */
    var cssRules = '{.w-sitelogo img { width: 200px !important; } ';
    
    /* create the style element */
    var styleElement = document.createElement('style');
    
    /* add style rules to the style element */
    styleElement.appendChild(document.createTextNode(cssRules));
    
    /* attach the style element to the document head */
    document.getElementsByTagName('head')[0].appendChild(styleElement);
    
    

    However, adding a media query in an attempt to only make the logo larger on large screen breaks it:

    
    /* Credit: davidwalsh.name */
    /* specify our style rules in a string */
    var cssRules = '@media only screen and (min-device-width: 1025px) {.w-sitelogo img { width: 200px !important; } }';
    
    /* create the style element */
    var styleElement = document.createElement('style');
    
    /* add style rules to the style element */
    styleElement.appendChild(document.createTextNode(cssRules));
    
    /* attach the style element to the document head */
    document.getElementsByTagName('head')[0].appendChild(styleElement);
    
    
  49. Nice talking about Stylesheets with JavaScript but there is compatibility issue with IE browser, and that create trouble for you.

  50. Gabriel

    I know this is a pretty old conversation, but you can actually just use innerHTML on the style element and it works

  51. Caution: some of the information in this article is wrong (or outdated). Specifically, -1 is not a valid index argument for insertRule (throws an exception). Also, it defaults to 0, not -1 when omitted. The correct index value for inserting a rule at end of sheet is styleSheet.cssRules.length

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