Theming with Constructed Style Sheets

Constructable Style Sheets are useful to inject and control styles via JavaScript.

Let’s assume we’re building a client-side, JavaScript-heavy component or application. (In rare cases, such transgressions might actually be warranted.) Our theme colors might reside in a JavaScript module:

export let light = {
    foreground: "#000",
    background: "#FFF",
    shadow: "#0008"

export let dark = {
    foreground: "#FFF",
    background: "#000"

Naturally we wanna turn those into CSS custom properties:

:root {
    &.theme-light {
        --color-foreground: #000;
        --color-background: #FFF;
        --color-shadow: #0008;

    &.theme-dark {
        --color-foreground: #FFF;
        --color-background: #000;

my-component {
    color: var(--color-foreground);
    background-color: var(--color-background);
    box-shadow: 0 0.2rem 0.8rem var(--color-shadow);

We might add those custom properties directly to our root element:

import { light, dark } from "./themes.js";

let colors = Math.random < 0.5 ? light : dark;

let root = document.documentElement;
for(let [name, value] of Object.entries(colors)) {`--color-${name}`, value);

Here we only apply a single theme’s values instead of relying on .theme-* classes as sketched out above; imagine there’s a button to toggle between light and dark value assignments.

This works fine for the most part, but there are two subtle issues:

Fortunately, there’s a simple way around that now: constructed style sheets.

let THEME = new CSSStyleSheet();

function applyTheme(colors) {
    let css = Object.entries(colors).
        map(([name, value]) => `--color-${name}: ${value};`).
    THEME.replace(`:root { ${css} }`);

While this capability was originally introduced to improve efficiency for Shadow DOM, it’s useful for manipulating style sheets in general. The downside is that this kind of string concatenation always feels a little icky compared to key-value assignments, though it doesn’t seem so bad in this context.