Creating and importing styles with constructable stylesheets and CSS module scripts
TL;DR
Create a new stylesheet from scratch and apply it to a HTML document:
const myStylesheet = new CSSStyleSheet();
myStylesheet.replaceSync('h1 {color: blue;}');
document.adoptedStyleSheets.push(myStylesheet);
Import a stylesheet from a .css
file and apply it to a HTML document:
import styles from "./styles.css" with { type: "css" };
document.adoptedStyleSheets.push(styles);
Introduction
It’s long been possible to create a <link>
or <style>
element using JavaScript.
Here’s an example adding a <link>
element to the <head>
:
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = './fancystyles.css';
document.head.appendChild(link);
You can also create a <style>
element and add styles using .textContent
and/or .insertRule
:
const styleElement = document.createElement('style');
styleElement.textContent = "h1 {color: green;}";
document.head.appendChild(styleElement);
styleElement.sheet.insertRule("h2 {color: blue;}");
Constructable stylesheets are a new approach.
Constructable stylesheets
You can create a stylesheet with the CSSStyleSheet()
constructor:
const myStylesheet = new CSSStyleSheet();
Optionally, an object of options can be passed to the constructor. In the following example the styles within the stylesheet will only apply if the users system is set to use dark mode:
const sheet = new CSSStyleSheet({media: "(prefers-color-scheme: dark)"});
You can add styles to the stylesheet synchonously or asynchronously:
// replace all styles synchronously:
myStylesheet.replaceSync('h1 {color: green;} body {background: pink;}');
The asynchronous version returns a promise that resolves with the CSSStyleSheet
object:
myStylesheet.replace('h1 {color: green;} body {background: pink;}')
.then(sheet => console.log('Successfully replaced styles', sheet))
.catch(err => console.error('Failed to replace styles:', err));
You apply the stylesheet to the documents adoptedStyleSheets
property, which is an array:
document.adoptedStyleSheets.push(myStylesheet);
When Constructable Stylesheets first shipped in Chrome adoptedStyleSheets
was a frozen array. Some older blog posts therefor use the following syntax:
document.adoptedStyleSheets = [...document.adoptedStyleSheets, myStylesheet];
adoptedStyleSheets
is no longer a frozen array, so you can use .push()
and other array methods. You could, for example, remove a particular stylesheet from adoptedStyleSheets
using filter
:
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(sheet => sheet !== myStylesheet); // removes myStylesheet
You can remove the last stylesheet with document.adoptedStyleSheets.pop()
, the first stylesheet with document.adoptedStyleSheets.unshift()
, etc.
When using either .replace
or .replaceSync
, @import
rules are ignored with a warning. You should not do the following:
myStylesheet.replace('@import url("styles.css");'); // Console warning
How, then, can we import a CSS file?
CSS module scripts
You can import a stylesheet with JavaScript using import attributes:
import myStylesheet from "./fancystyles.css" with { type: "css" };
The default export of a CSS module is a CSSStyleSheet
(the same sort of object that gets created by new CSSStyleSheet()
).
Just like we saw with the new CSSStyleSheet()
constructor example, applying the imported stylesheet to the HTML document is achieved via the adoptedStyleSheets
API:
document.adoptedStyleSheets.push(myStylesheet);
If the same CSS file is imported multiple times in your JavaScript code it will only be fetched, instantiated, and parsed a single time.
Dynamically importing a CSS module
The import statement shown above can only be used at the top level. import()
, by contrast, can be used inside a function or inside an if
statement, for example. import()
will dynamically load a module so that it is only evaluated when needed.
document.querySelector('button').addEventListener('click', async function() {
const stylesheet = await import('./extrastyles.css', { with: { type: 'css' } });
document.adoptedStyleSheets.push(stylesheet.default);
});
import()
returns a promise which fulfills with a module namespace object — an object containing all the exports from the module. The CSSStyleSheet
is the default export, so we access it with .default
.
Multiple stylesheets per file?
If you have dozens of import
statements making seperate requests for CSS files, it isn’t ideal for performance. That’s where the prospective addition of named exports comes in: it looks likely that we’ll eventually have a way to import different stylesheets from a single file.
Preloading CSS modules
When including a stylesheet in the traditional way using <link rel="stylesheet">
in the <head>
of a document, there’s no reason to preload as the resource is discovered quickly. When importing CSS using JavaScript, by contrast, preloading might be useful. CSS imported via JavaScript only loads after the JavaScript is parsed.
As with many other kinds of resources, you can preload CSS by adding a <link>
tag with rel="preload"
to the <head>
of your HTML:
<link rel="preload" href="fancystyles.css" as="style" />
Manipulating a CSSStyleSheet
with JavaScript
The API for manipulating stylesheets isn’t entirely new. Let’s recap some older ways of obtaining a CSSStyleSheet
.
Given the following HTML:
<head>
<meta charset="UTF-8">
<link id="my-link" rel="stylesheet" href="extra.css">
<style id="my-style">
body {
font-family: system-ui;
}
</style>
</head>
You can select a specific <style>
or <link>
element and get a CSSStyleSheet
object by referencing the element’s sheet
property:
const stylesheet1 = document.getElementById("my-link").sheet; // is a CSSStyleSheet
const stylesheet2 = document.getElementById("my-style").sheet; // is a CSSStyleSheet
To get all stylesheets you’ve embedded or linked to a HTML document using <style>
or <link>
elements, you can use document.styleSheets
which returns an Array-like StyleSheetList
. Each stylesheet in the list is represented by a CSSStyleSheet
object.
const firstStylesheet = document.styleSheets[0]; // is a CSSStyleSheet
The replace
and replaceSync
methods only work on a CSSStyleSheet
you’ve either created with new CSSStyleSheet()
or imported via a JavaScript import assertion. Both of the following lines of code fail:
document.getElementById("my-link").sheet.replace('h1 {color: green;}'); // DOMException: Failed to execute 'replace' on 'CSSStyleSheet': Can't call replace on non-constructed CSSStyleSheets.
document.styleSheets[0].replaceSync('h2 {color: blue;}'); // DOMException: Failed to execute 'replaceSync' on 'CSSStyleSheet': Can't call replaceSync on non-constructed CSSStyleSheets.
All other methods and properties work regardless of how you’ve obtained the CSSStyleSheet
.
Add rules to the stylesheet using the insertRule()
method:
const ruleIndex = myStylesheet.insertRule("h1 {color: green;}");
The return value of the .insertRule
method is the index of the rule. By default the rule is prepended at the start.
Delete a rule at a specified index:
myStylesheet.deleteRule(ruleIndex);
CSSStyleSheet
inherits properties from StyleSheet
. One inherited property that might come in useful if you want to toggle a stylesheet is the disabled
property. Rather than adding and removing a stylesheet from the adoptedStyleSheets
array, you could toggle the disabled property:
document.querySelector('button').addEventListener('click', function() {
myStylesheet.disabled = !myStylesheet.disabled;
});
Browser support
The adoptedStyleSheets
property and the CSSStyleSheet()
constructor have been supported since Firefox 101, Safari 16.4 and Chrome 73.
Chrome has supported import attributes, including support for CSS modules, since version 123. Safari 17.2 added support for import attributes but currently only supports JSON modules, not CSS modules. Import attributes are currently being implemented in Firefox.
There is a Rollup plugin for using CSS module scripts in browsers that lack support but it does not offer full parity with the native browser feature.
CSS modules (the open source project)
Confusingly, CSS Modules is also the name of a popular open source project for scoping CSS. That is not something that the web standard does, and there isn’t any relation or similarity between the standard and the open-source project. The web standard is sometimes referred to as “CSS Module Scripts”, which might help to avoid the confusion.