Programmatically Accessing the CSS Rules in Your Page's Stylesheets

It's not a scenario that comes up too often, but every once in a while, I find the need to programmatically access a CSS rule in one of my stylesheets (which includes rules embedded in the page via the <style> tag).

The major problem here is that there is only one mechanism available for accessing a CSSStyleSheetDeclaration object and its corresponding CSSRule collection.

And that (rather unfortunate) mechanism is by zero-based index, which means you need to know exactly where in the DOM a particular stylesheet lives and exactly where in said stylesheet the particular rule you're interested in lives.

Needless to say, accessing these sheets and rules by index is risky, since any update to your CSS may require an update to the code which accesses your CSSRule by index.

You might be thinking that one solution is to put the rule (or rules) that interest you in a predictable place (e.g. the very first rule of the very first stylesheet in your page), but even this is risky and error prone (I know; I did it, and it once came back to bite me).

In light of these issues, I wrote a function that, given a CSS selector rule, finds the the corresponding CSSRule (if it exists). Since this is not necessarily a cheap process, the results are cached so that future requests for the same rule are returned in constant time.

The full code is offered below, with explanations of anything interested embedded in comments.

The findCSSRule(..) function

var findCSSRule = function()
{  
   var cache = {};
   function worker(_selector, _media)
   {
      if (typeof cache[_selector] !== 'undefined')
      {
         return cache[_selector];      // return the cached rule if it exists
      }
      
      _media = _media || '';            // if _media isn't passed in, default to empty string
      
      var i, j,
      mediaText,
      sheet, rule, rules, rulesLen,
      sheets = document.styleSheets,
      sheetsLen = sheets.length;
      
      for (i = 0; i < sheetsLen; i++)
      {
         sheet = sheets[i];
         
         // accessing the media attribute of the stylesheet requires a bit of cross-browser trickery
         mediaText = typeof sheet.media.mediaText !== 'undefined' ? sheet.media.mediaText : sheet.media;
   
         // don't waste time with this sheet if its not of the right media type
         if (mediaText.indexOf(_media) === -1)
         {
            continue;
         }
         
         rules = sheet[prop];
         rulesLen = rules.length;
         for (j = 0; j < rulesLen; j++)
         {
            rule = rules[j];
            if (rule.selectorText === _selector)
            {
               return cache[_selector] = rule;
            }
         }
      }

      return null;        
   }

   // this function is returned if the browser doesn't provide a known
   // mechanism for accessing stylesheets and/or CSS rules
   function unsupported()
   {
      return null;
   }

   // make sure there's at least 1 stylesheet to search,
   // then prop will be "cssRules" (W3C), "rules" (IE), or null (unsupported)
   var test = document.styleSheets.length > 0 && document.styleSheets[0];
   var prop = test &&
      (typeof test.cssRules !== 'undefined' ? 'cssRules' :
      typeof test.rules !== 'undefined' ? 'rules' :
      null);

   // false prop (meaning no stylesheets or unsupported method of accessing the CSSRules),
   // means this function won't work, so rather than return the worker, return the unsupported function
   return prop ? worker : unsupported;
}();

Caveats:

This function isn't perfect by any means -- a few things need to be called out.

  1. Specifying media type could be more flexible: I'm just doing a simple indexOf(..) check on the stylesheet's media attribute, which means that if you want to filter by more than one media type, you'll need to specify the comma separated list yourself, and you'll need to make sure you do it in the right order if you want the indexOf(..) check to pass. A more robust solution might be to parse a comma separated string of media types for the user, or to allow the user to specify multiple media types in an array.
  2. Memory consumption could be an issue: I don't think it's likely, but if you're grabbing lots and lots of rules, the cache could become large and memory leaks could become an issue (since the cache is created in a closure). I'm not overly concerned about this though -- you can decide for yourself is this is a big deal.
  3. No mechanism for handling dynamically created styles: If you happen to load a stylesheet later in page's lifetime (after the initial load) this function is still going to return the unsupported() function, since it does its check once, up front, to decide whether or not accessing stylesheets/rules is feasible. I'm okay with this, but again, you can decide for yourself.
  4. Examples:

    Now that the function is written, a few examples are in order. Assume the following stylesheets:

    - stylesheet 0 - (media = "print")

    div>span
    {
       border: 10px solid red;
    }

    .christmas
    {
       background: green;
       color: red;
    }

    .pascha
    {
       background: purple;
       color: yellow;
    }

    - stylesheet 1 - (media = "screen")

    div>span
    {
       border: 5px dotted blue;
    }

    #main div~span
    {
       background: pink;
    }

    Accessing any of the above styles is fairly trivial.

    // returns the div>span rule from stylesheet 0
    var r1 = findCSSRule('div>span', 'print');
       
    // also returns the div>span rule from stylesheet 0 (since media default to empty string, the first stylesheet with this rule will match)
    var r2 = findCSSRule('div>span');
       
    // returns the div>span rule from stylesheet 1
    var r3 = findCSSRule('div>span', 'scren');
       
    // returns the .pascha rule from stylesheet 0
    var r4 = findCSSRule('.pascha');
       
    // returns null
    var r5 = findCSSRule('.halloween');
       
    // returns the #main div~span rule from stylesheet 1
    var r6 = findCSSRule('#main div~span');
       
    // returns null (b/c the rule doesn't exist in a stylesheet with media = "print"
    var r7 = findCSSRule('#main div~span', 'print');

    And there you have it!

    Comments, questions, suggestions, complaints? Bring 'em on!

4 Responses

  1. RichB Says:

    Another caveat would be cross-domain access. Firefox has policies which prevent JavaScript accessing the CSS cross-domain (eg when your CSS is stored on a CDN).

    I worked on a site where we styled Silverlight by pulling styling attributes from CSS. However, because we were cross-domain, this failed. So we had to style hidden DIVs and then pull the current style off these DIVs and re-apply them to the Silverlight objects.

  2. sstchur Says:

    Excellent point Rich. Thanks for calling that out, as I wasn't aware of it.

  3. how-to-do-css-programmatically | Savetime On Says:

    […] vs. Programmatically Programmatically setting the "float" CSS property in Javascript Programmatically Accessing the CSS Rules in Your Page's Stylesheets How to add Html Headers […]

  4. Eljay Says:

    I often have same selector in different .CSS files, tweaking different style. As a kluge, for my purposes, given specific style rules a unique name that I can use programmatically will allow me to modify the exact style rule I want. For example, the selector: #{0}, #main div~span {…} then I can locate that rule by #{0}, and another by #{1}, and another #{2} … by convention. Anyway, thank you very much for the tip on getting to the styles!

Got something to say?

Please note: Constructive criticism is welcome. Rude or vulgar comments however, are not and will be removed during moderation.