66

I'm developing a Chrome extension, and I'd like users to be able to add their own CSS styles to change the appearance of the extension's pages (not web pages). I've looked into using document.stylesheets, but it seems like it wants the rules to be split up, and won't let you inject a complete stylesheet. Is there a solution that would let me use a string to create a new stylesheet on a page?

I'm currently not using jQuery or similar, so pure Javascript solutions would be preferable.

grigno
  • 3,128
  • 4
  • 35
  • 47
Dan Hlavenka
  • 3,697
  • 8
  • 42
  • 60

6 Answers6

133

There are a couple of ways this could be done, but the simplest approach is to create a <style> element, set its textContent property, and append to the page’s <head>.

/**
 * Utility function to add CSS in multiple passes.
 * @param {string} styleString
 */
function addStyle(styleString) {
  const style = document.createElement('style');
  style.textContent = styleString;
  document.head.append(style);
}

addStyle(`
  body {
    color: red;
  }
`);

addStyle(`
  body {
    background: silver;
  }
`);

If you want, you could change this slightly so the CSS is replaced when addStyle() is called instead of appending it.

/**
 * Utility function to add replaceable CSS.
 * @param {string} styleString
 */
const addStyle = (() => {
  const style = document.createElement('style');
  document.head.append(style);
  return (styleString) => style.textContent = styleString;
})();

addStyle(`
  body {
    color: red;
  }
`);

addStyle(`
  body {
    background: silver;
  }
`);

IE edit: Be aware that IE9 and below only allows up to 32 stylesheets, so watch out when using the first snippet. The number was increased to 4095 in IE10.

2020 edit: This question is very old but I still get occasional notifications about it so I’ve updated the code to be slightly more modern and replaced .innerHTML with .textContent. This particular instance is safe, but avoiding innerHTML where possible is a good practice since it can be an XSS attack vector.

Liam Newmarch
  • 3,935
  • 3
  • 32
  • 46
  • I would have come up with innerHTML eventually, but the second snippet you provided is really cool! – Dan Hlavenka Mar 28 '13 at 23:12
  • Both snippets give me error: TypeError: document.body is null document.body.appendChild(node); – angry kiwi Dec 03 '13 at 04:52
  • Couldn't you append to the node's innerHTML to add continuously? e.g something like `node.innerHTML = node.innerHTML + " " + str;`? – brandito Aug 07 '18 at 01:55
21

Thanks to this guy, I was able to find the correct answer. Here's how it's done:

function addCss(rule) {
  let css = document.createElement('style');
  css.type = 'text/css';
  if (css.styleSheet) css.styleSheet.cssText = rule; // Support for IE
  else css.appendChild(document.createTextNode(rule)); // Support for the rest
  document.getElementsByTagName("head")[0].appendChild(css);
}

// CSS rules
let rule  = '.red {background-color: red}';
    rule += '.blue {background-color: blue}';

// Load the rules and execute after the DOM loads
window.onload = function() {addCss(rule)};

fiddle

John R Perry
  • 3,916
  • 2
  • 38
  • 62
8

Have you ever heard of Promises? They work on all modern browsers and are relatively simple to use. Have a look at this simple method to inject css to the html head:

function loadStyle(src) {
    return new Promise(function (resolve, reject) {
        let link = document.createElement('link');
        link.href = src;
        link.rel = 'stylesheet';

        link.onload = () => resolve(link);
        link.onerror = () => reject(new Error(`Style load error for ${src}`));

        document.head.append(link);
    });
}

You can implement it as follows:

window.onload = function () {
    loadStyle("https://fonts.googleapis.com/css2?family=Raleway&display=swap")
        .then(() => loadStyle("css/style.css"))
        .then(() => loadStyle("css/icomoon.css"))
        .then(() => {
            alert('All styles are loaded!');
        }).catch(err => alert(err));
}

It's really cool, right? This is a way to decide the priority of the styles using Promises.

Or, if you want to import all styles at the same time, you can do something like this:

function loadStyles(srcs) {
    let promises = [];
    srcs.forEach(src => promises.push(loadStyle(src)));
    return Promise.all(promises);
}

Use it like this:

loadStyles([
    'css/style.css',
    'css/icomoon.css'
]);

You can implement your own methods, such as importing scripts on priorities, importing scripts simultaneously or importing styles and scripts simultaneously. If i get more votes, i'll publish my implementation.

If you want to learn more about Promises, read more here

  • Interesting indeed. Here you wait for the end of a css to load the next though Wouldn't it be more efficient to load it all in one go? – Pascal Ganaye Oct 27 '20 at 23:48
  • It is definitely more efficient to load them all at once. However, in some cases there may be a need to assign a certain priority. I have published a simultaneous style load implementation. – Iglesias Leonardo Nov 04 '20 at 16:42
  • I guess this is better solution because promises will run asynchronously and not be render blocking as opposed to the accepted solution – user1020069 Jul 27 '21 at 17:00
5

I think the easiest way to inject any HTML string is via: insertAdjacentHTML

// append style block in <head>

const someStyle = `
<style>
    #someElement { color: green; }
</style>
`;

document.head.insertAdjacentHTML('beforeend', someStyle);
Gruber
  • 2,196
  • 5
  • 28
  • 50
2

I had this same need recently and wrote a function to do the same as Liam's, except to also allow for multiple lines of CSS.

injectCSS(function(){/*
    .ui-button {
        border: 3px solid #0f0;
        font-weight: bold;
        color: #f00;
    }
    .ui-panel {
        border: 1px solid #0f0;
        background-color: #eee;
        margin: 1em;
    }
*/});

// or the following for one line

injectCSS('.case2 { border: 3px solid #00f; } ');

The source of this function. You can download from the Github repo. Or see some more example usage here.

My preference is to use it with RequireJS, but it also will work as a global function in the absence of an AMD loader.

Pat Cullen
  • 135
  • 1
  • 3
  • 8
    Your method of using a multi-line comment is... interesting. I'm not sure how "good" an idea it is, but it sure is interesting. – Dan Hlavenka May 07 '14 at 14:11
  • 1
    I would say it's a dangerous misuse. CSS allows multiline comments too, so be careful when you copy-paste styles from existing stylesheets. – djjeck Apr 15 '15 at 21:15
0

Create a style tag and add the css styles into the textContent property as a string. Append these in the document head. Boom you are good to go.

var styles = document.createElement("style");
styles.setAttribute("type", "text/css");
styles.textContent = `#app{background-color:lightblue;}`;
document.head.appendChild(styles);
Kalnode
  • 9,386
  • 3
  • 34
  • 62
Rohit Sharma
  • 565
  • 3
  • 7