Dynamic CSS Classes in Svelte

Written

A common request from Svelte newcomers is how to use template expressions in a <style> block. This usually comes up in the context of custom themes.

<!-- If Svelte allowed it, you might do something like this -->
<style>
  .note {
    color: {noteColor || '#000000'};
    background-color: {noteBgColor};
  }

  .note::before {
    content: "{notePrefix} || 'A note'";
    margin-right: {notePrefixSpacing};
  }
</style>

The problem is that Svelte only compiles template syntax in the HTML section of the component. The style section of a component does not use templates, so this method can not be used to generate styles at runtime.

But there is a very good alternative.

CSS Variables 🔗

CSS has native support for defining values at runtime. Finalized in 2015, CSS variables let you create style rules that depend on values defined up the element tree from where they are applied.

<style>
  .note {
    color: var(--note-color, var(--note-other-color, tomato));
    background-color: var(--note-bg-color, lightgray);
  }
</style>
<p class="note">This is important!</p>

CSS variables always start with --. The var function gets the value of a variable, and falls back to the second argument (the value after the comma) if the variable is not set. This fallback value can also be a variable reference.

In this example an element with the class note will use the variables --note-color and --note-bg-color if they are set. The color attribute will fall back to --note-other-color if --note-color is not set, and finally fall back to the color tomato, while the background-color will use lightgray if --note-bg-color is not set.

Of course, variables aren’t very useful if we don’t set them. Variables can be set either as part of other classes or in the style attributes of elements.

<style>
    .note {
        color: var(--note-color, tomato);
        background-color: var(--note-bg-color, lightgray);
    }

    .yellow-theme {
        --note-color: black;
        --note-bg-color: khaki;
    }

    .purple-note-text {
        --note-color: rebeccapurple;
   }
</style>

<div style="--note-color:green;--note-bg-color:tomato">
  <h1 class="note" style="font-weight:600">Christmas!</h1>

  <div class="yellow-theme">
    <p class="note">For that yellow notepad look.</p>

    <p class="note purple-note-text">Or with purple</p>
  </div>
</div>

When Variables Apply 🔗

Although variables apply to the element that sets them and all of its children, any classes or styles that use variables only read those variables at the point where they are set.

Here, the div element gets the note class, and the span sets --note-color:green. But setting the variable there does not modify how the class applies, and so the text appears as the default color tomato instead of green.

<style>
  .note { color: var(--note-color, tomato); }
</style>

<div class="note">
  <span style="--note-color:green">Not Green!</span>
</div>

The REPL example below is similar to the first REPL, but the note class is applied only on the topmost element, and so the CSS variables set in the child elements do not take effect. To use the note class with the new values, it needs to be applied again at or below where the variables are reassigned.

<style>
    .note {
        color: var(--note-color, tomato);
        background-color: var(--note-bg-color, lightgray);
    }

    .yellow-theme {
        --note-color: black;
        --note-bg-color: khaki;
    }

    .purple-note-text {
        --note-color: rebeccapurple;
   }
</style>

<!-- Although the note class applies to all the child elements, setting
the CSS variables in child elements does not apply unless we reapply the note class again. -->
<div class="note" style="--note-color:green;--note-bg-color:tomato">
    <h1 style="font-weight:600">Christmas!</h1>

    <div class="yellow-theme">
     <p>For that yellow notepad look.</p>

     <p class="purple-note-text" style="--note-bg-color:white">Or with purple</p>
    </div>

</div>

Setting CSS Variables through Svelte 🔗

As established, we can’t use Svelte to modify the rules inside a <style> tag, but we can generate element attributes in the template. And so this gives us the ability to set CSS variables using Svelte template syntax, e.g. <span style="--note-color:{noteColor}">.

This Svelte REPL example sets variables dynamically.

Fallbacks for Internet Explorer 🔗

It’s important to note that CSS variables are not supported in Internet Explorer, so if you need to support it, your stylesheets should provide reasonable fallback defaults.

.classname {
  color: red;
  color: var(--color, red);
}

With this pair of rules, Internet Explorer will see the first rule and set the color to red. It will then ignore the second color rule since it’s unable to understand the syntax.

Newer browsers will parse both rules, but the second one will take precedence. Note that the first rule is ignored completely, even if there is no value for the --color variable, so the red fallback must still be present at the end of the var clause if you want to provide a default value.

Global CSS Variables 🔗

Sometimes CSS variables need to be globally scoped. The easiest solution is to have a top-level div in your application that contains all the CSS variables, and use the techniques above to set the variables on that div.

If for some reason this is not possible, you can set your variables directly on the <html> element with syntax like this: $: document.documentElement.style.cssText = styles.

The original example used the <body> element. Thanks to Kevv on Twitter for asking about this since it works with the :root selector as well.

When you have multiple places that need to write CSS variables in this way, they should be written using a global manager instead so that all the variable settings will be combined properly.

A simple manager like this can combine style settings from multiple sources. Here’s a full example: Svelte REPL Document Styles.

const cssVars = new Map();

function refresh() {
  let values = [];
  for(let [key, value] of cssVars) {
    values.push(`--${key}:${value}`);
  }
  document.documentElement.style.cssText = values.join(';');
}

export function set(name, value) {
  cssVars.set(name, value);
  refresh();
}

export function del(name) {
  cssVars.delete(name);
  refresh();
}

The various components in the application can then use this single module to manage the global CSS variables, and so long as the variable names are unique, everything will work.

Multiple Classes 🔗

If the styles required fall into a finite and reasonably small set of values, you don’t even need to use CSS variables. Svelte makes it easy to enable classes on an element based on some expression.

<style>
    .green {
        color: white;
        background-color: green;
     }

    .red: {
        color: black;
        background-color: red;
    }
</style>

<script>
    let isRed = false;
    $: className = isRed ? 'red' : 'green';
</script>

<label><input type="checkbox" bind:value={isRed} /> Red?</label>

<!-- Svelte's built-in class: syntax -->
<div class:red={isRed} class:green={!isRed}>Text</div>

<!-- Or using template syntax in the class attribute -->
<div class={className}>Text</div>

Creating Rules from Scratch 🔗

Browsers also support creating rules from scratch with the CSS Object Model (CSSOM), also known as CSS-in-JS. I don’t recommend using this approach with Svelte, since you lose the component class scoping behavior of Svelte and most of the packages that use CSSOM spend a lot of effort recreating behaviors that Svelte provides natively.

Also, most of the CSSOM helper libraries are specifically linked to React or other frameworks, but some, such as Aphrodite, can work independently. So it is an option if you really want to go that way.

Further Reading 🔗

My palette transformer project (source) is a small example of dynamically setting the theme for an entire site using CSS variables.

Once you get comfortable with CSS variables, you may also want to check out other ways to upgrade your CSS knowledge. The CSS Tricks blog is a great place to start.


Thanks for reading! If you have any questions or comments, please send me a note on Twitter.