none
How to inject STYLE into a page

    Question

  • Is it a supported scenario to inject a style tag into the body like this?

    <div id="injectionDiv">&nbsp;</div>

    <script type="text/javascript">
    document.getElementById("injectionDiv").innerHTML='<style type="text/css">.myClass{background-color:green;}</style><div class="myClass">&nbsp;</div>';
    </script>

    If so, then why isn't the style applied?

    Friday, November 10, 2006 1:17 AM

Answers

  • You can inject style via script, but it can be a little tricky.

    Internally, Internet Explorer treats the <style> tag as a NoScope element, which means (according to a rather opaque comment in the source) that "no text in a textrun should point to it." Examples of other tags that have this attribute are HTML comments (<!-- -->) and SCRIPT tags. All NoScope elements are removed from the beginning of the parsed HTML before it is injected with innerHTML or insertAdjacentHTML. To prevent this from happening, you must include at least one scoped element at the beginning of the injected HTML.

    So, using the example above, all we have to do is put the style block after the element it modifies.

    document.getElementById("injectionDiv").innerHTML='<div class="myClass">&nbsp;</div><style type="text/css">.myClass{background-color:green;}</style>';

    That said, you really should put all style rules in the HEAD for strict compliance with XHTML. Doing this can also be a little tricky because you cannot inject HTML into the HEAD or STYLE element directly. The best way to dynamically add styles to a page is to wait for the document to load, and then create the rule in a new style sheet.

    window.onload = function()
    {
        document.createStyleSheet().addRule('.myClass', 'background-color:blue!important;');
    }

     

     

    Friday, November 10, 2006 1:29 AM

All replies

  • You can inject style via script, but it can be a little tricky.

    Internally, Internet Explorer treats the <style> tag as a NoScope element, which means (according to a rather opaque comment in the source) that "no text in a textrun should point to it." Examples of other tags that have this attribute are HTML comments (<!-- -->) and SCRIPT tags. All NoScope elements are removed from the beginning of the parsed HTML before it is injected with innerHTML or insertAdjacentHTML. To prevent this from happening, you must include at least one scoped element at the beginning of the injected HTML.

    So, using the example above, all we have to do is put the style block after the element it modifies.

    document.getElementById("injectionDiv").innerHTML='<div class="myClass">&nbsp;</div><style type="text/css">.myClass{background-color:green;}</style>';

    That said, you really should put all style rules in the HEAD for strict compliance with XHTML. Doing this can also be a little tricky because you cannot inject HTML into the HEAD or STYLE element directly. The best way to dynamically add styles to a page is to wait for the document to load, and then create the rule in a new style sheet.

    window.onload = function()
    {
        document.createStyleSheet().addRule('.myClass', 'background-color:blue!important;');
    }

     

     

    Friday, November 10, 2006 1:29 AM
  • Here's a second example that attempts to inject a SCRIPT tag, also considered to be NoScope by Internet Explorer, using innerHTML.

    <div id="injectionDiv">&nbsp;</div>

    <script type="text/javascript">
    alert(document.documentElement.outerHTML);
    document.getElementById("injectionDiv").innerHTML='<script defer>alert("hello");</' + 'script>';
    alert(document.documentElement.outerHTML);
    </script>

    I have used an alert to show me the before and after effects. As it turns out, the source of the page doesn't change at all.

    To work around this situation, just insert a dummy element (for example, a hidden input field) before the opening SCRIPT tag.

    document.getElementById("injectionDiv").innerHTML='<input type="hidden"/><script defer>alert("hello");</' + 'script>';

    Friday, November 10, 2006 1:41 AM
  • WARNING: This post contains inaccurate information.
    Please read follow-up post below.

     

    One final post on this subject. There are limitations and caveats to what you can do with a styleSheet object through script, namely:

    1. You can create a styleSheet, but you cannot delete one.
    2. The addRule method simply adds new rule; it doesn't update existing rules or merge rules under the same selector.
    3. You can retrieve objects from the styleSheets collection by ID; however, you can neither create a STYLE element with an ID, nor change the ID after the element is created. (The ID attribute is read-only on styleSheet objects!)

    Consider what happens when a page in a sub-frame adds a style rule to its parent page using a naive combination of createStyleSheet and addRule. Each time the sub-frame changed, it would add a duplicate rule to a new STYLE element. Since there's no easy way to detect whether the stylesheet has already been created, eventually IE returns a script error when it has had enough. Sub-optimal.

    If I started with a known quantity of style rules that were applied in all cases, I should only create the style sheet once. Fortunately, I can use TITLE to get around the limitations of the ID attribute. The following script retrieves a style sheet by title and creates a new styleSheet if necessary.

    function createCustomStyleSheet(t)
    {
       var ssa = document.styleSheets;
       for (var idx=0; idx<ssa.length; idx++)
       {
          if (ssa[idx].title == t)
             return ssa[idx];
       }

       // not found, create one
       var ss = document.createStyleSheet();

       // ADD RULES HERE

       ss.title = t; // instead of ID, which is read-only
       return ss;
    }

    Now, what if I wanted to remove rules dynamically? Since I cannot delete the styleSheet object, I could simply remove each rule one at a time, like this:

    var cnt = ss.rules.length;
    while(cnt--)
       ss.removeRule(cnt);

    This is where things get a little tricky. There used to be a cross-domain scripting vulnerability in IE6 that allowed you to load any random file whether it parsed as CSS or not, and then read it with the rules collection or cssText property. As a result, IE7 has been patched in such a way that it causes script errors if you attempt to read from those properties.

    The following code works well in both IE6 and IE7.

    function clearSheet(ss)
    {
       var bAccessDenied = false;
        
       // This variation works on IE6
       try {
          var cnt = ss.rules.length;
          while(cnt--)
             ss.removeRule(cnt);
       }
       catch(e) {
          // alert(e.description);
          if((e.number&0xffff) == 5)
             bAccessDenied = true;
       }
     
       // This variation works on IE7
       if (bAccessDenied)
       {
          try {
             while(true)
                ss.removeRule(0);
          }
          catch(e) {
             // alert("No more rules.");
          }
       }
     
       return ss;
    }

    With these functions in place, I can now safely create and remove rules in a controlled way.

    var ss = createCustomStyleSheet("localStyle");  // unique ID
    clearStyleSheet(ss); // remove previously added rules
    ss.addRule('.myClass', 'background-color:blue!important;');

    As you can see, inline styles via injected HTML are still quite a bit easier to manage.

    It's up to you to decide which approach to use.

    Wednesday, November 15, 2006 1:54 AM
  • I have learned some things since my "final word" post that I simply must share. I feel a little stupid for not figuring this stuff out sooner, but no better time than the present!  (I thank Justin Rogers for setting me straight.)

     

    I made several claims in my last installment to this thread that I now must RETRACT.

    1. You cannot delete stylesheet objects. WRONG.

    Having retrieved a stylesheet object from the document, you can delete it with removeNode(true) in IE, or with the following DOM-compliant analog:

    Code Snippet

        // Remove an existing stylesheet by ID

        var ss = document.getElementById('myStyleSheet');
        if (ss) ss.parentNode.removeChild(ss);

     

    1. You cannot set the ID of stylesheet objects. WRONG.

    It may seem obvious, but IE is the only browser that allows you to create a stylesheet object with the document.createStyleSheet() method. When it does so, the object is returned to you fully formed; in other words, the requisite HTML has already been added to the document in the HEAD section. The ID attribute is read-only if you access the object as a stylesheet object, but NOT if it's a regular DOM element.

     

    Best to avoid createStyleSheet() altogether, and use the cross-browser friendly version here:

    Code Snippet

        // Create stylesheet object and append to HEAD

        ss = document.createElement("STYLE");
        ss.id = 'myStyleSheet';
        document.documentElement.firstChild.appendChild(ss);

     

    1. You cannot change stylesheet contents with innerHTML. RIGHT! but only on IE...

    For "security" purposes in IE, the innerHTML property of stylesheet objects is read-only. However, this is not the case universally. For a cross-browser approach, first try setting the rules all at once with innerHTML. If that fails, set the rules individually with addRule, as follows:

    Code Snippet

        var rules = '.myClass {background-color:#AAD2DE;} p { color:red} '

        try
        {
            ss.innerHTML = rules;
        }
        catch(exc)
        {

            var parts = rules.split(/\s*[{}]\s*/);
            for (var i=0; i<parts.length; i+=2)
                ss.styleSheet.addRule(parts[i],parts[i+1]);
        }

     

     

    As you can see the resulting code is MUCH cleaner and easier to comprehend.

    Friday, September 21, 2007 10:00 PM
  • One more thing.

     

    If you simply must inject a fully formed STYLE tag using a scoped innerHTML string as described above, avoid relative paths to resources such as background images. Until temporary elements are appended to the document, IE uses the context of the top-level document to download resources. Although it may be apparent to you which page is building the STYLE rules, an unexpected frameset can throw you a curve ball.

     

    The solution is to always append the style element to the document BEFORE assigning it any rules.

     

     

    Saturday, September 22, 2007 12:03 AM
  • Great write up John, I had a forked version of the code based on "if (document.createStyleSheet)" to write out an IE and a  DOM version. Your way simplified my code no end.

    Cheers!
    Wednesday, November 14, 2007 1:08 PM
  • If we are going for cross-browser, properly done code, lets go all the way.

    Code Block

    // Create stylesheet object and append to HEAD    

        ss = document.createElement('style');

        ss.setAttribute('id', 'myStyleSheet');

        document.getElementsByTagName('head')[0].appendChild(ss);


    Changed the style tag to be lower case, because the year 2000 was 7 years ago now, and as soon as IE supports XHTML developers will be too!

    Also changed the append line, to select the head tag explicitly, since there may be some comment, or whitespace tags before the head.

    ;-)


    Wednesday, November 14, 2007 2:36 PM
  • Very cool. Thanks for the DOM/XHTML compliance tips!

     

     

    Wednesday, November 14, 2007 5:36 PM
  • One thing to note on this is that if you're using commas to split classes,id's etc with the same style e.g.

    #element,
    .class1,
    .class2 {
    style: value;
    }

    then these rules need to be applied seperately in addRule by doing something like
    Code Block

    split(parts[i],',')

    and looping through the array.

    My code is different enough from that shown above so I don't want to post and cause confusion but mail me if you want a look.
    Friday, December 14, 2007 3:35 PM
  • Good catch.

     

    Friday, December 14, 2007 10:05 PM
  • Thanks for update!
    Javaman
    Tuesday, February 02, 2010 12:24 AM
  • Wonderful!

    I am using in http://www.atasozlerianlamlari.com web site and working very nice!

    Thank you for all!
    Friday, February 19, 2010 10:09 AM
  • Here's a second example that attempts to inject a SCRIPT tag, also considered to be NoScope by Internet Explorer, using innerHTML.

     


    <SCRIPT type=text/javascript>
    alert(document.documentElement.outerHTML);
    document.getElementById("injectionDiv").innerHTML='<script defer>alert("hello");</' + 'script>';
    alert(document.documentElement.outerHTML);
    </SCRIPT>

    I have used an alert to show me the before and after effects. As it turns out, the source of the page doesn't change at all.

    To work around this situation, just insert a dummy element (for example, a hidden input field) before the opening SCRIPT tag.

    document.getElementById("injectionDiv").innerHTML='<INPUT type=hidden> <SCRIPT defer>alert("hello");</' + 'script>';

    </SCRIPT>


    It is good for reference, Thanks!
    Tuesday, September 14, 2010 1:55 AM