Skip to main content
menu

BMT Systems

  WEB & JAVASCRIPT  

Mobile Navigation Controls

Using Dynamic Style Sheets To Modify CSS with a Strict 'style-src' Content Security Policy

Thomas M. Brodhead

The Problem with Inline Styles

Inline styles are set directly on an element with the style attribute:

brightness_high content_paste
<p style="background-color: red">Inline styles are insecure!</p>

They have the highest style specificity, “trumping” anything set in a CSS style rule, and thus guaranteeing that they are applied. (See What the Heck Is CSS Specificity? for an overview of the topic if you’re unfamiliar. An excellent and instructive tool worth bookmarking is the CSS Specificity Calculator.)

But inline styles are a security risk. Google states emphatically that they are dangerous, and even disallows them in AMP sites. Kyle Robinson Young’s article Disable Inline Styles explains the risks fully, and laments that they are often part and parcel of SPA-generated code.

You may have been disciplined and worked hard not to use inline styles on DOM elements in your HTML, relegating all styling to CSS that reference DOM elements by attributes. But there may be elements whose styling requires dynamic changes that no CSS rule can provide, e.g., altering the width or height of an item when some condition obtains. In that case, the typical solution is to inject an inline style on the element using JavaScript. But this would prevent you from using a strict style-src Content Security Policy that would protect your site and its visitors.

A strict style-src policy is the goal, and inline styles that are set with JavaScript are the main impediment to achieving it.

This article provides a solution. Using an often overlooked JavaScript mechanism for changing CSS with a dynamically created style sheet, dynamic inline styling may be avoided altogether.

To be clear, this is not about setting a style directly on an element using JavaScript. We’ll be exploring a JavaScript-based alternative method to that. Carefully applied, it will harmonize with a strict style-src Content Security Policy, a Holy Grail for web developers.

Prerequisites for Understanding the Method

If you understand CSS specificity and have a structured game plan for writing CSS rules, you’re well-positioned for proceeding. There are many competing methodologies for writing successful CSS, for example, BEM and OOCSS. Among the plain-vanilla approaches, many web coders avoid using IDs altogether and instead employ classes exclusively. The best argument for doing this is explained in Hacks for dealing with specificity, in which the author exhorts you to:

Make heavy use of classes because they are the ideal selector: low specificity (or rather, all classes have the same specificity, so you have a level playing field), great portability, and high reusability.

The key phrase here is “level playing field.” This point is central to the class-only CSS philosophy. If no IDs are used, then there will be no surprise stylings arising from a forgotten ID-based style rule “trumping” an intended class-based rule. It also means that if you’ve applied classes strategically, you will only need two class selectors at most (thus a numerical weight of 20) to style any element on the page. (The proof of this comes from set theory, and will be the subject of a future article.)

In all of this, there are two important concepts for our JavaScript method of element-specific styling. One is that if a new CSS rule is to be applied to a targeted element, then the rule must be more specific than other rules that would target it. How you create that rule is dependent on how your CSS has been composed. The other important concept is that if an element could be styled by two or more different selectors that have the same numerical specificity, then the style rule that comes LAST in your CSS is the one that is employed.

With that in mind, some possible scenarios and methods for rule writing would be:

  • If you’ve used classes exclusively in your CSS selectors, and if you have limited each CSS selector’s value to a maximum of 20, then a new rule for the element that has a specificity of 20 added AFTER the other rules will be the one used to style the element.
  • If you’ve used classes exclusively in your CSS rules, but your rule specificity varies and could be anything from 20 to 99 (because you’ve needed and used pseudo-classes, which have a weight of 20, as well as elements and pseudo-elements, which have a weight of 1), then you need only to add a unique ID to the targeted element and create an ID-based style rule. (IDs have a specificity weight of 100, and thus a single ID would “trump” any combination of class-based CSS rules—with or without pseudo-classes, elements, and pseudo-elements—so long as their combined value is less than 100, which should normally be the case.)
  • If you’ve used a mixture of IDs and classes in your CSS, you can retrieve the selector of the rule that normally affects the target element by examining the element in the Developer’s Tools of Chrome or similar browsers. That selector (e.g., div#whoops.retrograde.on-par) with different style settings could be duplicated AFTER the static CSS for the site (i.e., it could be added on a dynamically created style sheet that comes after the static CSS for the site), and it would therefore “trump” the static selector by virtue of coming last.

These are just three ways of deriving a selector that will override the selectors already present in your static CSS. It’s unimportant which strategy you choose so long as it works.

Prerequisites for Understanding CSP Style Security

The essential concept of the 'style-src' CSP directive is to allow the webmaster to control which styles are applied to the site. Style rules issued from the host domain and its intended third-party resources are to be allowed, and all others are to be denied. Mozilla’s article on the 'style-src' Content Security Policy offers the best overview of the topic, but with one caveat as of this writing (April, 2018). The article ends with this disclaimer:

The 'unsafe-eval' source expression controls several style methods that create style declarations from strings. If 'unsafe-eval' isn’t specified with the style-src directive, the following methods are blocked and won’t have any effect:

  • CSSStyleSheet.insertRule()
  • CSSGroupingRule.insertRule()
  • CSSStyleDeclaration.cssText

That is not true. The CSSStyleSheet.insertRule function, which we’ll be using, does in fact work when 'unsafe-eval' is not present in the 'style-src' CSP.

Having explained that, our goal is to employ this header, where the * is a hexadecimal nonce:

brightness_high content_paste
Content-Security-Policy: style-src 'self' 'nonce-*'

This will restrict styles to those originating from the home domain and those approved for use by the site with a nonce attribute on a style element. The nonce will be issued by our site, and it will be different each time the site is visited or whenever the browser is refreshed.

Getting Down to Work: Creating a Dynamic Style Sheet

Let’s say that you’ve employed the following code in your JavaScript:

brightness_high content_paste
element.style.display = 'none';

That would write a style rule directly to the element. But it will not work with the restrictive 'style-src' we want to employ. The styling will fail to be applied because of the CSP directive. So, we’re going to accomplish the same styling by writing a dynamically generated rule to a dynamically generated style sheet.

Creating a dynamic style sheet is easy:

brightness_high content_paste
var styleElement = document.createElement('style');

Boom! It exists, here referenced by the variable styleElement. It is formally defined as a CSSStyleSheet, and it will be accessed via the .sheet property of the element. We'll invoke that property when setting a dynamicStyleSheet variable, described below.

All of your CSS is referenced by “sheets,” whether they were brought in with link elements in your HTML or with style elements in your HTML. Either type is a “sheet” to the DOM and to JavaScript, and you may select any of them by indexes corresponding to the order in which they were encountered or created.

Thus, once you append the styleElement in the code example above to the DOM, it will become the last in the series of style “sheets” that you’ll access with JavaScript. So that you may immediately reference it anywhere in your code, you’ll want to store it in a variable declared in the highest enclosing function. That way it can be accessed in all of your functions. Let’s call that variable dynamicStyleSheet and declare it at the top of your code:

brightness_high content_paste
(function () {
    'use strict';
// Declare 'dynamicStyleSheet' with the other variable declarations at the top of your topmost enclosing function
    var dynamicStyleSheet;

// Define 'appendToCSS' as property of global object window so it can be accessed in other routines:
    window.appendToCSS = function () {
        var fragment;
        var styleElement;

// If dynamicStyleSheet variable with unique ID doesn't yet exist, create it (here, '#dynamic-style-element' by default):
        if (!document.querySelector('#dynamic-style-element')) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
// Add unique ID:
            styleElement.setAttribute('id', 'dynamic-style-element');
// Append it to the head.
            document.head.appendChild(fragment);
// Save the reference to the sheet.
            dynamicStyleSheet = styleElement.sheet;
        }
    };
}());

Notice that dynamicStyleSheet is defined as the .sheet property of the style element we've created. The dynamicStyleSheet variable references a single “sheet” on which you may write your rules dynamically.

Adding a Nonce to the Dynamic Style Sheet

We next need to nonce the sheet so that it will be allowed by the 'style-src' directive sent to the browser as a header. While it’s possible to write headers directly to the HEAD element of the HTML, it’s better to send them using a server-side directive (e.g., the header function in PHP). That means you’ll need to retrieve the nonce by special means.

The easiest way to do this is to put the nonce in a dedicated data-nonce attribute, and that attribute should belong to a META element in the HEAD whose main attribute is name=web_author. This technique is discussed in full in a dedicated section of a previous article at this site, Getting Your Head in Order: Custom Data. Read that section for a full explanation of how to construct the META element, and use that same methodology to set the nonce in a data-nonce attribute each time the page is loaded or refreshed.

You may then retrieve the nonce earlier in your code using a routine like this:

brightness_high content_paste
// Look for web_author META tag and collect nonce from it if present:
var metaNameWebAuthor = document.querySelector('meta[name=web_author]');
var nonce;

if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
    nonce = metaNameWebAuthor.dataset.nonce;
}

As with the dynamicStyleSheet variable, the nonce variable should be declared and defined in the outermost enclosing function so that it may be retrieved throughout your code.

There are other ways to do this. You may not want to expose the nonce in your HTML. (Notice that Chrome will consume all standard nonces on elements—not the data-nonce attribute we’re proposing—when they are parsed by the browser. To see this, compare nonce attributes on elements in the HTML using view-source: in the URL vs. the DOM structure as exposed in Chrome’s developer’s tools, which shows the actual state of the DOM after it has been parsed, rendered, and mutated by JavaScript.)

You could equally well employ an AJAX post routine to retrieve the nonce from the server, which would entail writing the nonce to a special file for retrieval by the JavaScript via AJAX after the DOM has been parsed. That method is employed by this site, and a full description of the technique is unnecessary for this discussion, except to acknowledge that it can be done that way.

But getting back to our dynamic style sheet, we’ll want to add the nonce to it so that it passes the restrictive 'style-src' directive:

brightness_high content_paste
(function () {
    'use strict';
    var dynamicStyleSheet;
// NEW: variables:
    var metaNameWebAuthor;
    var nonce;

// NEW:
// Look for web_author META tag and collect nonce from it if present:
    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

// Define 'appendToCSS' as property of global object window so it can be accessed in other routines:
    window.appendToCSS = function () {
        var fragment;
        var styleElement;

        if (!document.querySelector('#dynamic-style-element')) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
            styleElement.setAttribute('id', 'dynamic-style-element');

// if a nonce is in use, set the nonce attribute to it:
            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }

            document.head.appendChild(fragment);
            dynamicStyleSheet = styleElement.sheet;
        }
    };
}());

Inserting a New Rule to the Dynamic Style Sheet

Now that we have a dynamic style sheet to house our dynamic CSS, we need a mechanism for adding those rules to the style sheet. The insertRule operator of our style element (i.e., a CSSStyleSheet element) allows us to do that using this syntax:

brightness_high content_paste
dynamicStyleSheet.insertRule(rule, index);

The documentation only indirectly clarifies that the rule must have a specific formatting. The formatting is nothing novel, but it is strict. The selector must be a single-space-separated listing of attributes followed by the style rules in curly braces, each with a single space character separating the keys and values in the style rules, hence:

brightness_high content_paste
.enclosing-class .specific-class { background-color: red; margin: 3em; padding-top: 1em; }

Not:

brightness_high content_paste
.enclosing-class    .specific-class{background-color:red;    margin:    3em;padding-top:1em}

So, we’ll need to ensure our injected style rule follows this format.

Also, if the index parameter is 0 (the default), then the style rule is injected at the beginning of the style sheet, which is not what we want. We want to place each new style rule at the end of the style sheet. This is accomplished by explicitly stating the last index of the sheet as the index argument. We’ll have to keep track of that with our code.

Composing the Function

We’ve already covered a bit of the code needed to do this, so let’s now refine the function that will do the work. It will receive three strings: a selector string, a declarations string, and a default ID for the sheet named sheetIdName:

brightness_high content_paste
window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {

};

Go ahead and edit the dynamic style sheet creation code we covered earlier as follows:

brightness_high content_paste
(function () {
    'use strict';
    var dynamicStyleSheet;
    var metaNameWebAuthor;
    var nonce;

    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

// NEW:
// Add 'selector', 'declarations', and 'sheetIdName' arguments to the function:
    window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {
        var fragment;
        var styleElement;

// Here we reference 'sheetIdName', which allows for custom naming of sheets:
        if (!document.querySelector('#' + sheetIdName)) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
            styleElement.setAttribute('id', sheetIdName);

            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }

            document.head.appendChild(fragment);
            dynamicStyleSheet = styleElement.sheet;
        }
    };
}());

Don’t forget that dynamicStyleSheet is declared outside of this function—best located at the top of the outermost enclosing function—so that we may call that variable anywhere in our JavaScript.

Formatting the selector and properties Arguments in the Function

We’ll next need to ensure that the argument for the selector parameter and the argument for the declarations parameter are well formed. We can do this with judicious use of regular expressions:

brightness_high content_paste
// Reconstruct the 'selector' argument with only one space character between consecutive attributes:
        selector = selector.split(/\s+/).join(' ');

// We also must ensure the 'declarations' argument has the correct formatting,
// e.g., '{ max-height: 10px; color: blue; }'

// So, first remove all '{' and '}' characters and trim the result:
        declarations = declarations.replace(/[{}]/g, '').trim();

// Next, ensure that the last style property ends in a semicolon:
        if (declarations.substr((declarations.length - 1), 1) !== ';') {
            declarations += ';';
        }

// Then reconstruct the declarations argument with a single space character between each attribute:
        declarations = '{ ' + declarations.split(/\s+/).join(' ') + ' }';

// Now construct the CSS rule from the properly formatted 'selector' and 'declarations' arguments:
        var rule = selector + ' ' + declarations;

Add that to our growing function:

brightness_high content_paste
(function () {
    'use strict';
    var dynamicStyleSheet;
    var metaNameWebAuthor;
    var nonce;

    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

    window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {
        var fragment;
        var styleElement;

        if (!document.querySelector('#' + sheetIdName)) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
            styleElement.setAttribute('id', sheetIdName);

            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }

            document.head.appendChild(fragment);
            dynamicStyleSheet = styleElement.sheet;
        }

// Reconstruct the 'selector' argument with only one space character between consecutive attributes:
        selector = selector.split(/\s+/).join(' ');
// We also must ensure the 'declarations' argument has the correct formatting,
// e.g., '{ max-height: 10px; color: blue; }'
// So, first remove all '{' and '}' characters and trim the result:
        declarations = declarations.replace(/[{}]/g, '').trim();
// Next, ensure that the last style property ends in a semicolon:
        if (declarations.substr((declarations.length - 1), 1) !== ';') {
            declarations += ';';
        }
// Then reconstruct the declarations argument with a single space character between each attribute:
        declarations = '{ ' + declarations.split(/\s+/).join(' ') + ' }';
// Now construct the CSS rule from the properly formatted 'selector' and 'declarations' arguments:
        var rule = selector + ' ' + declarations;

    };
}());

Getting the Last Index of the Sheet

We’ll need to locate the last index of the dynamic style sheet so we can inject our rule into it. This merely requires querying the length property of the cssRules property of the “sheet,” so let’s make a general purpose function for that:

brightness_high content_paste
// helper function:
var lastCssRuleIndex = function (sheet) {
    return sheet.cssRules.length;
};

Add that to our growing function:

brightness_high content_paste
(function () {
    'use strict';
    var dynamicStyleSheet;
    var metaNameWebAuthor;
    var nonce;

    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

    window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {
        var fragment;
        var styleElement;

// NEW:
// helper function:
        var lastCssRuleIndex = function (sheet) {
            return sheet.cssRules.length;
        };

        if (!document.querySelector('#' + sheetIdName)) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
            styleElement.setAttribute('id', sheetIdName);

            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }

            document.head.appendChild(fragment);
            dynamicStyleSheet = styleElement.sheet;
        }

        selector = selector.split(/\s+/).join(' ');
        declarations = declarations.replace(/[{}]/g, '').trim();
        if (declarations.substr((declarations.length - 1), 1) !== ';') {
            declarations += ';';
        }
        declarations = '{ ' + declarations.split(/\s+/).join(' ') + ' }';
        var rule = selector + ' ' + declarations;

    };
}());

We’ll also need two additional utility functions in the outermost enclosing function, one, isNumeric, to test whether a value is a numeric, the other, sameArrayContents, to see if the contents of two arrays are identical:

brightness_high content_paste
var isNumeric = function (value) {
    return !Number.isNaN(value - parseFloat(value));
};

var sameArrayContents = function (array1, array2) {
    return (array1.length === array2.length) && (array1.every(function (value, index) {
        return value === array2[index];
    }));
};

Now add those to our growing function:

brightness_high content_paste
(function () {
    'use strict';
    var dynamicStyleSheet;
    var metaNameWebAuthor;
    var nonce;

    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

// NEW:
// helper functions (located in enclosing function because of general utility):
    var isNumeric = function (value) {
        return !Number.isNaN(value - parseFloat(value));
    };

    var sameArrayContents = function (array1, array2) {
        return (array1.length === array2.length) && (array1.every(function (value, index) {
            return value === array2[index];
        }));
    };


    window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {
        var fragment;
        var styleElement;

        var lastCssRuleIndex = function (sheet) {
            return sheet.cssRules.length;
        };

        if (!document.querySelector('#' + sheetIdName)) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
            styleElement.setAttribute('id', sheetIdName);

            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }

            document.head.appendChild(fragment);
            dynamicStyleSheet = styleElement.sheet;
        }

        selector = selector.split(/\s+/).join(' ');
        declarations = declarations.replace(/[{}]/g, '').trim();
        if (declarations.substr((declarations.length - 1), 1) !== ';') {
            declarations += ';';
        }
        declarations = '{ ' + declarations.split(/\s+/).join(' ') + ' }';
        var rule = selector + ' ' + declarations;

    };
}());

Adding To or Revising Our Dynamic Sheet

The next step is a bit tricky. If the rule we wish to add is new—i.e., if its selector does not match an existing selector in the sheet—then we may simply append it to the end of the sheet. But we may wish to revise an existing rule. This might happen in cases where we are repeatedly changing the size of an element in response to other changes in the DOM, and we are merely revising a rule that is a size setting. In that case, we don’t want to append a new rule with a selector that matches the earlier rule’s selector. Now, that would work, in fact, because of the final caveat to the specificity rules of CSS: the last rule with equal specificity to other rules will be the rule that is applied, something discussed earlier in this article. But if we did that—i.e, repeatedly appending a revised rule to the end of our dynamic sheet—we’d end up with a dynamic style sheet that would grow out of control. Instead, we want to delete the old, matching rule and append the new rule to the end of the sheet.

The JavaScript code to accomplish this would be:

brightness_high content_paste
// Now, extract the properties ('margin-top', etc.) from the declarations block.
// Find strings of non-white-space characters ending in colons, which will be properties.
// Add them to the propertiesArray:

// regular expression for series of non-white-space characters terminating in a colon:
var regex = /(\S+):(?!\S+)/g;
var propertiesArray = [];
// feed in the declarations from the enclosing function (the function we've been building):
var regexMatch = regex.exec(declarations);
// fill the properties array:
while (regexMatch !== null) {
    propertiesArray.push(regexMatch[1]);
    regexMatch = regex.exec(declarations);
}

// Next, see whether the selector already exists in the dynamic style sheet.
// Set variables, assuming failure (to be revised on success):
var selectorExists = false;
var propertiesMatch = false;

// Cycle through the dynamic style sheet;
// look for matching selectors, and then determine whether the style settings are different or not:
Object.keys(dynamicStyleSheet.cssRules).forEach(function (key) {
    var sheetPropertiesArray;
    var sheetSelector;
    var position;

// Does the selector text match?
// If so, then are there the same properties ('margin-left', etc.) in the rule set?
// --> We want to replace an existing rule-set if the selector is the same and all of the properties are the same, regardless of their values!

// Each sheet has a selector (e.g., 'body .my-class')
// For that selector, reconstruct the rule:
    sheetSelector = dynamicStyleSheet.cssRules[key].selectorText;

// Does the sheetSelector match the selector in the rule set being processed
    if (sheetSelector === selector) {
// Fill sheetPropertiesArray with all properties (but not values!) for the sheetSelector under consideration:
        sheetPropertiesArray = [];
// The initial indexes in the array are numeric; they have our declarations, and we want those only.
// In order to break from the forEach loop once we reach the non-numeric indexes, we'll need to throw
// an exception in a try/catch block to get out of it early:
        try {
            Object.keys(dynamicStyleSheet.cssRules[key].style).forEach(function (index) {
                if (isNumeric(index)) {
                    sheetPropertiesArray.push(dynamicStyleSheet.cssRules[key].style[index]);
                } else {
                    throw 'End of numeric indices.';
                }
            });
        } catch (ignore) {
            // for testing only:
            // console.log(ignore);
        }
// If the set of properties (but not values!) is the same between the rule-set being processed
// and the rule-set in the sheet, then we've got a match:
        if (sameArrayContents(propertiesArray, sheetPropertiesArray)) {
            selectorExists = true;
            propertiesMatch = true;
// 'position' records the location in the dynamic styleSheet where the rule to be deleted is located:
            position = key;
        }
    }
});

// Now, armed with this info, we may proceed:
// If the selector exists:
if (selectorExists) {
// And the properties are identical (irrespective of the values!):
    if (propertiesMatch) {
// Delete the old rule...
        dynamicStyleSheet.deleteRule(position);
// ...and add the new one at the end of the sheet, using our helper function (lastCssRuleIndex):
        dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
    }
} else {
// If the selector isn't there at all, just add the new rule to the end of the sheet:
    dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
}

Now add that code to our growing function:

brightness_high content_paste
(function () {
    'use strict';
    var dynamicStyleSheet;
    var metaNameWebAuthor;
    var nonce;

    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

    var isNumeric = function (value) {
        return !Number.isNaN(value - parseFloat(value));
    };

    var sameArrayContents = function (array1, array2) {
        return (array1.length === array2.length) && (array1.every(function (value, index) {
            return value === array2[index];
        }));
    };

    window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {
        var fragment;
        var styleElement;

        var lastCssRuleIndex = function (sheet) {
            return sheet.cssRules.length;
        };

        if (!document.querySelector('#' + sheetIdName)) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
            styleElement.setAttribute('id', sheetIdName);

            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }

            document.head.appendChild(fragment);
            dynamicStyleSheet = styleElement.sheet;
        }

        selector = selector.split(/\s+/).join(' ');
        declarations = declarations.replace(/[{}]/g, '').trim();
        if (declarations.substr((declarations.length - 1), 1) !== ';') {
            declarations += ';';
        }
        declarations = '{ ' + declarations.split(/\s+/).join(' ') + ' }';
        var rule = selector + ' ' + declarations;

// Now, extract the properties ('margin-top', etc.) from the declarations block.
// Find strings of non-white-space characters ending in colons, which will be properties.
// Add them to the propertiesArray:

// regular expression for series of non-white-space characters terminating in a colon:
        var regex = /(\S+):(?!\S+)/g;
        var propertiesArray = [];
// feed in the declarations from the enclosing function (the function we've been building):
        var regexMatch = regex.exec(declarations);
// fill the properties array:
        while (regexMatch !== null) {
            propertiesArray.push(regexMatch[1]);
            regexMatch = regex.exec(declarations);
        }

// Next, see whether the selector already exists in the dynamic style sheet.
// Set variables, assuming failure (to be revised on success):
        var selectorExists = false;
        var propertiesMatch = false;
        var position;

// Cycle through the dynamic style sheet;
// look for matching selectors, and then determine whether the style settings are different or not:
        Object.keys(dynamicStyleSheet.cssRules).forEach(function (key) {
            var sheetPropertiesArray;
            var sheetSelector;

// Does the selector text match?
// If so, then are there the same properties ('margin-left', etc.) in the rule set?
// --> We want to replace an existing rule-set if the selector is the same and all of the properties are the same, regardless of their values!

// Each sheet has a selector (e.g., 'body .my-class')
// For that selector, reconstruct the rule:
            sheetSelector = dynamicStyleSheet.cssRules[key].selectorText;

// Does the sheetSelector match the selector in the rule set being processed
            if (sheetSelector === selector) {
// Fill sheetPropertiesArray with all properties (but not values!) for the sheetSelector under consideration:
                sheetPropertiesArray = [];
// The initial indexes in the array are numeric; they have our declarations, and we want those only.
// In order to break from the forEach loop once we reach the non-numeric indexes, we'll need to throw
// an exception in a try/catch block to get out of it early:
                try {
                    Object.keys(dynamicStyleSheet.cssRules[key].style).forEach(function (index) {
                        if (isNumeric(index)) {
                            sheetPropertiesArray.push(dynamicStyleSheet.cssRules[key].style[index]);
                        } else {
                            throw 'End of numeric indices.';
                        }
                    });
                } catch (ignore) {
                    // for testing only:
                    // console.log(ignore);
                }
// If the set of properties (but not values!) is the same between the rule-set being processed
// and the rule-set in the sheet, then we've got a match:
                if (sameArrayContents(propertiesArray, sheetPropertiesArray)) {
                    selectorExists = true;
                    propertiesMatch = true;
// 'position' records the location in the dynamic styleSheet where the rule to be deleted is located:
                    position = key;
                }
            }
        });

// Now, armed with this info, we may proceed:
// If the selector exists:
        if (selectorExists) {
// And the properties are identical (irrespective of the values!):
            if (propertiesMatch) {
// Delete the old rule...
                dynamicStyleSheet.deleteRule(position);
// ...and add the new one at the end of the sheet, using our helper function (lastCssRuleIndex):
                dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
            }
        } else {
// If the selector isn't there at all, just add the new rule to the end of the sheet:
            dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
        }

    };
}());

Manually Hoist the Variables

As a finishing touch, hoist the variable definitions to the top of the function. This prevents any problems regarding variable scoping, and is a best practice when writing JavaScript: Always Hoist Your JavaScript Variables Explicitly To Avoid Unexpected Scoping Problems!

brightness_high content_paste
(function () {
    'use strict';
// NEW: hoisted & alphabetized variables:
    var dynamicStyleSheet;
    var isNumeric;
    var metaNameWebAuthor;
    var nonce;
    var sameArrayContents;

    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

    isNumeric = function (value) {
        return !Number.isNaN(value - parseFloat(value));
    };

    sameArrayContents = function (array1, array2) {
        return (array1.length === array2.length) && (array1.every(function (value, index) {
            return value === array2[index];
        }));
    };

    window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {
// NEW: hoisted & alphabetized variables:
        var fragment;
        var lastCssRuleIndex;
        var position;
        var propertiesArray;
        var propertiesMatch;
        var regex;
        var regexMatch;
        var rule;
        var selectorExists;
        var styleElement;

// NEW: 'var' hoisted to top of function:
        lastCssRuleIndex = function (sheet) {
            return sheet.cssRules.length;
        };

        if (!document.querySelector('#' + sheetIdName)) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
            styleElement.setAttribute('id', sheetIdName);

            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }

            document.head.appendChild(fragment);
            dynamicStyleSheet = styleElement.sheet;
        }

        selector = selector.split(/\s+/).join(' ');
        declarations = declarations.replace(/[{}]/g, '').trim();
        if (declarations.substr((declarations.length - 1), 1) !== ';') {
            declarations += ';';
        }
        declarations = '{ ' + declarations.split(/\s+/).join(' ') + ' }';
// NEW: 'var' hoisted to top of function:
        rule = selector + ' ' + declarations;

// NEW: 'var's hoisted to top of function:
        regex = /(\S+):(?!\S+)/g;
        propertiesArray = [];
        regexMatch = regex.exec(declarations);

        while (regexMatch !== null) {
            propertiesArray.push(regexMatch[1]);
            regexMatch = regex.exec(declarations);
        }

// NEW: 'var's hoisted to top of function:
        selectorExists = false;
        propertiesMatch = false;

        Object.keys(dynamicStyleSheet.cssRules).forEach(function (key) {
            var sheetPropertiesArray;
            var sheetSelector;

            sheetSelector = dynamicStyleSheet.cssRules[key].selectorText;

            if (sheetSelector === selector) {
                sheetPropertiesArray = [];
                try {
                    Object.keys(dynamicStyleSheet.cssRules[key].style).forEach(function (index) {
                        if (isNumeric(index)) {
                            sheetPropertiesArray.push(dynamicStyleSheet.cssRules[key].style[index]);
                        } else {
                            throw 'End of numeric indices.';
                        }
                    });
                } catch (ignore) {
                    // for testing only:
                    // console.log(ignore);
                }
                if (sameArrayContents(propertiesArray, sheetPropertiesArray)) {
                    selectorExists = true;
                    propertiesMatch = true;
                    position = key;
                }
            }
        });

        if (selectorExists) {
            if (propertiesMatch) {
                dynamicStyleSheet.deleteRule(position);
                dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
            }
        } else {
            dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
        }

    };
}());

The complete function, with annotations

Now that we’ve covered all the component elements of the function atomically, here is the complete function again, this time with all commentary added for reference:

brightness_high content_paste
/*!
 appendToCSS.js - v2.0.1

 Copyright (c) 2018-2019 Thomas M. Brodhead <https://bmt-systems.com>
 Released under the MIT license

 Date: 2019-06-29
*/
(function () {
    'use strict';
    var dynamicStyleSheet;
    var isNumeric;
    var metaNameWebAuthor;
    var nonce;
    var sameArrayContents;

// Look for web_author META tag and collect nonce from it if present:
    metaNameWebAuthor = document.querySelector('meta[name=web_author]');
    if (metaNameWebAuthor && Boolean(metaNameWebAuthor.dataset.nonce)) {
        nonce = metaNameWebAuthor.dataset.nonce;
    }

// General-purpose utility functions needed by routine:
    isNumeric = function (value) {
        return !Number.isNaN(value - parseFloat(value));
    };

    sameArrayContents = function (array1, array2) {
        return (array1.length === array2.length) && (array1.every(function (value, index) {
            return value === array2[index];
        }));
    };

// Add this to 'window' global object so it can be accessed in modules
    window.appendToCSS = function (selector, declarations, sheetIdName = 'dynamic-style-element') {
        var fragment;
        var lastCssRuleIndex;
        var position;
        var propertiesArray;
        var propertiesMatch;
        var regex;
        var regexMatch;
        var rule;
        var selectorExists;
        var styleElement;

// helper function:
        lastCssRuleIndex = function (sheet) {
            return sheet.cssRules.length;
        };

// If dynamicStyleSheet variable with unique ID doesn't yet exist, create it (here, '#dynamic-style-element' by default):
        if (!document.querySelector('#' + sheetIdName)) {
            fragment = document.createDocumentFragment();
            styleElement = document.createElement('style');
            fragment.appendChild(styleElement);
// Add unique ID:
            styleElement.setAttribute('id', sheetIdName);
// if a nonce is in use, set the nonce attribute to it:
            if (nonce) {
                styleElement.setAttribute('nonce', nonce);
            }
// Append it to the head.
            document.head.appendChild(fragment);
// NO, this will not work: styleElement = document.head.appendChild(styleElement);
// Save the reference to the sheet.
            dynamicStyleSheet = styleElement.sheet;
        }

// Reconstruct the 'selector' argument with only one space character between consecutive attributes:
        selector = selector.split(/\s+/).join(' ');
// We also must ensure the 'declarations' argument has the correct formatting,
// e.g., '{ max-height: 10px; color: blue; }'
// So, first remove all '{' and '}' characters and trim the result:
        declarations = declarations.replace(/[{}]/g, '').trim();
// Next, ensure that the last style property ends in a semicolon:
        if (declarations.substr((declarations.length - 1), 1) !== ';') {
            declarations += ';';
        }
// Then reconstruct the declarations argument with a single space character between each attribute:
        declarations = '{ ' + declarations.split(/\s+/).join(' ') + ' }';
// Now construct the CSS rule from the properly formatted 'selector' and 'declarations' arguments:
        rule = selector + ' ' + declarations;

// Now, extract the properties ('margin-top', etc.) from the declarations block.
// NB: The values are unimportant.
// Finds strings of non-white-space characters ending in colons; add them to propertiesArray:
        regex = /(\S+):(?!\S+)/g;
        propertiesArray = [];
        regexMatch = regex.exec(declarations);
        while (regexMatch !== null) {
            propertiesArray.push(regexMatch[1]);
            regexMatch = regex.exec(declarations);
        }

// Next, see whether the selector already exists in the dynamic style sheet.
// Set variables, assuming failure (to be revised on success):
        selectorExists = false;
        propertiesMatch = false;
// Cycle through the dynamic style sheet;
// look for matching selectors, and then determine whether the style settings are different or not:
        Object.keys(dynamicStyleSheet.cssRules).forEach(function (key) {
            var sheetPropertiesArray;
            var sheetSelector;

// Does the selector text match?
// If so, then are there the same properties ('margin-left', etc.) in the rule set?
// --> We want to replace an existing rule-set if the selector is the same and all of the properties are the same, regardless of their attributes!

// Each sheet has a selector (e.g., 'body .my-class')
// For that selector, reconstruct the rule:
            sheetSelector = dynamicStyleSheet.cssRules[key].selectorText;

// Does the sheetSelector match the selector in the rule set being processed
            if (sheetSelector === selector) {
// Fill sheetPropertiesArray with all properties (but not values!) for the sheetSelector under consideration:
                sheetPropertiesArray = [];
// The initial indices in the array are numeric; they have our declarations, and we want those only.
// In order to break from the forEach loop once we reach the non-numeric indices, we'll need to throw
// an exception in a try/catch block to get out of it early:
                try {
                    Object.keys(dynamicStyleSheet.cssRules[key].style).forEach(function (index) {
                        if (isNumeric(index)) {
                            sheetPropertiesArray.push(dynamicStyleSheet.cssRules[key].style[index]);
                        } else {
                            throw 'End of numeric indices.';
                        }
                    });
                } catch (ignore) {
                    //console.log(ignore);
                }
// If the set of properties (but not values!) is the same between the rule-set being processed
// and the rule-set in the sheet, then we've got a match:
                if (sameArrayContents(propertiesArray, sheetPropertiesArray)) {
                    selectorExists = true;
                    propertiesMatch = true;
// 'position' records the location in the dynamic styleSheet where the rule to be deleted is located:
                    position = key;
                }
            }
        });

// Now, armed with this info, we may proceed:
// If the selector exists:
        if (selectorExists) {
// And the properties are identical (irrespective of the values!):
            if (propertiesMatch) {
// Delete the old rule...
                dynamicStyleSheet.deleteRule(position);
// ...and add the new one at the end of the sheet, using our helper function (lastCssRuleIndex):
                dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
            }
        } else {
// If the selector isn't there at all, just add the new rule to the end of the sheet:
            dynamicStyleSheet.insertRule(rule, lastCssRuleIndex(dynamicStyleSheet));
        }
    };

}());

Minified:

brightness_high content_paste
/*
 appendToCSS.js - v2.0.1

 Copyright (c) 2018-2019 Thomas M. Brodhead <https://bmt-systems.com>
 Released under the MIT license

 Date: 2019-06-29
*/
(function(){var a,e;if((e=document.querySelector("meta[name=web_author]"))&&e.dataset.nonce)var k=e.dataset.nonce;var p=function(a){return!Number.isNaN(a-parseFloat(a))};var q=function(a,b){return a.length===b.length&&a.every(function(a,f){return a===b[f]})};window.appendToCSS=function(f,b,c){c=void 0===c?"dynamic-style-element":c;var e,h;var l=function(a){return a.cssRules.length};if(!document.querySelector("#"+c)){var g=document.createDocumentFragment();var d=document.createElement("style");g.appendChild(d);d.setAttribute("id",c);k&&d.setAttribute("nonce",k);document.head.appendChild(g);a=d.sheet}f=f.split(/\s+/).join(" ");b=b.replace(/[{}]/g,"").trim();";"!==b.substr(b.length-1,1)&&(b+=";");b="{ "+b.split(/\s+/).join(" ")+" }";d=f+" "+b;c=/(\S+):(?!\S+)/g;var m=[];for(g=c.exec(b);null!==g;)m.push(g[1]),g=c.exec(b);var n=h=!1;Object.keys(a.cssRules).forEach(function(b){if(a.cssRules[b].selectorText===f){var c=[];try{Object.keys(a.cssRules[b].style).forEach(function(d){if(p(d))c.push(a.cssRules[b].style[d]);else throw"End of numeric indices.";})}catch(r){}q(m,c)&&(n=h=!0,e=b)}});h?n&&(a.deleteRule(e),a.insertRule(d,l(a))):a.insertRule(d,l(a))}})();

Using the appendToCSS Function in Your Code

Now that we have a function for adding rules to a dynamic style sheet, let’s discuss how to use it. Let’s say that you currently have JavaScript code that assigns an inline style like this:

brightness_high content_paste
document.querySelector('.my-class').style.display = 'none';

You’d eliminate that line of code entirely and replace it with this line of code:

brightness_high content_paste
appendToCSS('.my-class', '{ display: none; }');

...and it would accomplish the same thing, except you'd be able to use the safer, restrictive 'style-src' CSP directive.

But say you don’t know what CSS selector would be guaranteed to style your element. If you’ve only been using classes, as discussed earlier in this article, you could create a unique ID, add that to the element, and then create a dynamic style rule for that ID. Let’s code that now.

A helper function for generating a random alphanumeric sequence would be useful for this purpose, and the following function, inspired from this Stack Overflow discussion, provides inspiration for the function:

brightness_high content_paste
// Generate 5-character alphanumeric string:
var fiveRandomAlphaNumerics = function () {
    return (Math.random().toString(36) + '00000000000000000').slice(2, 7);
};

To ensure accessibility, this helper function should be placed in the outermost enclosing scope of the routines that would use it.

Armed with this helper function, a means to style the targeted element described above—returned with document.querySelector('.my-class') in that discussion, but it could be targeted by any means—would be:

brightness_high content_paste
// generate a unique ID:
var id = 'dynamic-id-' + fiveRandomAlphaNumerics();
// Add the selector+rule to the dynamic style sheet:
appendToCSS('#' + id, '{ display: none; }');
// Set the ID on the targeted element as an attribute:
document.querySelector('.my-class').setAttribute('id', id);

But what if the targeted element already has an ID? An element can’t have more than one ID, so if it does already, you’d need to employ that ID on a new rule added to the end of the dynamic style sheet. That won’t interfere with earlier stylings of the element accomplished by rules employing that ID (although, again, styling with IDs in your main, static CSS is discouraged, as it prevents a level playing field for CSS specificity).

So, for a given element selected in your JavaScript by arbitrary means, a routine for selecting and styling the element ID would be:

brightness_high content_paste
// See if there's a style attribute on the element and if it's not empty, which is sometimes the case!
if (element.hasAttribute('style') && (element.getAttribute('style') !== '')) {
// Check to see if it has an ID:
    if (element.hasAttribute('id')) {
// If so, retrieve the ID and use it for the style rule for the element:
        appendToCSS('#' + element.getAttribute('id'), '{ ' + element.getAttribute('style') + ' }');
    } else {
// If it doesn't have an ID, use the helper function for generating an original ID:
        id = 'dynamic-id-' + fiveRandomAlphaNumerics();
// Set the ID on the element as an attribute:
        element.setAttribute('id', id);
// Append the desired style rule to the dynamic style sheet, using the ID as the selector:
        appendToCSS('#' + id, '{ ' + element.getAttribute('style') + ' }');
    }
}

N.B.: Variations on this last piece of code may allow you to loop through and update DOM elements dynamically generated by other JavaScript libraries you are employing. This in turn would allowyou to enforce a strict 'style-src' CSP for your entire site when you are using third-party JavaScript libraries.

Final Notes on Revising Inline Styling of External JavaScript Routines

If you are self-hosting third-party JavaScript libraries that create DOM elements with inline styles, it would seem carte blanche that the last JavaScript routine described above may be used to style those elements dynamically. But as of this writing (April, 2018), that’s only true for Chrome, Opera, and IE. It’s not true for Firefox and Edge.

In response to a restrictive 'style-src' CSP, Chrome and Opera allow dynamically created elements with inline styles to be appended to the DOM. However, in response to the CSP, those browsers simply do not apply the styles specified by the inline style attributes. That is how they enforce the CSP. (IE is in the same category as Chrome and Opera here only because the 'style-src' CSP has no affect on it.)

On the other hand, Firefox and Edge respond to the restrictive 'style-src' CSP differently: they prevent the inline styles from being written to the newly created DOM elements altogether. Instead, empty style attributes are created in their place. For example, if style="display:none" is specified in an inline style on a dynamically created element, perhaps appended to the DOM with appendChild or a similar operator, an empty inline style style="" will be created as an attribute in Firefox and Edge. In contrast, Chrome and Opera would create the specified inline style on the element, but they simply would not response to it.

This prevents our routine from working on DOM elements created by third-party JavaScript libraries: you can’t simply loop through those elements, searching for inline styles and moving them into a dynamically created style sheet; the styles will have been stripped from the element tags when the HTML+JavaScript is run on Firefox and Edge.

A possible workaround would be to edit the third-party JavaScript code itself, renaming inline style attributes to data-style attributes. Those attributes would be written to the dynamically created DOM elements when the HTML+JavaScript is realized in all browsers. You could then loop through the newly created elements and look not for style attributes but for data-style attributes, and place their style rules into a dynamically created style sheet, creating IDs as selectors, etc., as described before. This in turn would bypass the method that Firefox and Edge employ for restricting inline styles in response to the strict style-src CSP. This may or may not be possible, depending on whether you’re able to host the third-party JavaScript on your own server, and then whether the license for the code allows that kind of modification.

7 April 2018
Last Updated: 29 June 2019

Aside