SvelteKit Theme Switch

When adding Dark Mode to my new SvelteKit project, there were a few issues I ran into when creating the theme switch, but I came up with a solution that I'm quite proud of, so I'd like to share it.

The solution is comprised of 4 main parts:

  • Svelte Store
  • Theme Switch Component
  • Style Switcher
  • Theme Styles

Before we begin, I'd like to point out that while this tutorial uses CSS Custom Properties (a.k.a. CSS Variables) for theming, the solution works with any theming method, as long as the styles are defined in a global stylesheet.

Also, I'll be using TypeScript, so for those using plain JS, simply remove the type definitions.

Types

The first thing we'll do is to define our themes. We can do this in the global type definitions (src/globals.d.ts) so that we have access to the types throughout our project.

src/globals.d.ts
type Theme = 'system' | 'light' | 'dark';

What we're doing here is declaring a global type called Theme that we can access from anywhere in our project. This means if we define a variable type as Theme, then we can only assign the values 'system', 'light', or 'dark' to it.

Apart from 'system', you can choose your theme values to your heart's desire. You're also not limited to just two, so experiment away!

The 'system' value here is important: we want the user to be greeted with their preferred theme, so we want the default theme to correspond to their Operating System color scheme.

Svelte Store

Now that we've got type definitions out of the way, we can move on to the heart of the theme switch: the theme store. The theme store is a Svelte Store.

src/lib/stores.ts
import { writable } from 'svelte/store';

export const theme = writable('system');

Here, we're creating a Svelte Store called theme and assigning it the default value of 'system'. Again, it is important that 'system' is the default so that the user's preferences are respected.

Theme Switch Component

We can now use the Svelte Store we created in our Theme Switch Component.

src/lib/components/ThemeSwitch.svelte
<script lang="ts">
    import { theme } from '$lib/stores';

    const themes = [
        { title: 'System', value: 'system' },
        { title: 'Light', value: 'light' },
        { title: 'Dark', value: 'dark' },
    ];
</script>

<select bind:value="{$theme}">
    {#each themes as { value, title }}
        <option {value}>{title}</option>
    {/each}
</select>

<style>
    /* your styles here */
</style>

There's a lot going on here, so I think a quick walkthrough is in order.

We first import theme from '$lib/stores'. $lib/stores is a path alias for src/lib/stores.svelte, the file in which we created our theme Svelte Store.

We now want to modify the value of theme. We could do this by calling theme.set(x), where x is the value we want theme to be (In this setup, 'system', 'light', or 'dark'). However, there is an easier way: the $ shortcut. By referencing a Svelte Store using a $ prefix, it becomes mutable.

(If a value is mutable, that means it can be changed by assigning a new value to it. In JavaScript, for example, let and var create mutable variables, whereas const creates immutable ones.)

Since $theme is mutable, we use the svelte binding bind:value to get theme to track the changes to the value of the selected option. The browser does most of the heavy lifting in this case, since all we need to do is read the value attribute.

Style Switcher

We now have a Svelte Store that stores the theme value, and a Theme Switch component that updates the theme value, so all that remains is the functionality for changing the theme based on the theme value.

The way I went about this is swapping stylesheets in the <head> of the generated document.

src/routes/__layout.svelte
<script lang="ts">
    import { theme } from '$lib/stores';
</script>

<svelte:head>
    <meta name="color-scheme" content={$theme == 'system' ? 'light dark' : $theme} />
    <link rel="stylesheet" href={`/theme/${$theme}.css`} />
</svelte:head>

<slot />

What we're doing here loading a CSS stylesheet dynamically based on the current theme value. For example, on page load, the previous code will generate the following:

<head>
    <meta name="color-scheme" content="light dark" />
    <link rel="stylesheet" href="/theme/system.css" />
</head>

Theme Styles

The only thing that remains now is to define the styles of our project. We can do this anywhere in the static/ directory, as long as we remember to adjust the path in <link rel="stylesheet" href={'/theme/${$theme}.css'} /> accordingly.

If we follow the path convention I set up, we get the following structure:

static
└── theme
    ├── system.css
    ├── light.css
    └── dark.css

In light.css and dark.css (or whatever you choose to call you themes), we style our project accordingly. An example:

light.css
:root {
    --lightest: 100%;
    --lighter: 80%;
    --light: 60%;
    --strong: 40%;
    --stronger: 20%;
    --strongest: 0%;
}

While these are straightforward, the file that requires more attention is system.css. This is because there we need to worry about system preferences. While it is easy to access the user's preference using the prefers-color-scheme media query, we need to keep in mind here that there are only two predefined choices, light and dark. Hence we need to style accordingly:

system.css
@media (prefers-color-scheme: light) {
    :root {
        --lightest: 100%;
        --lighter: 80%;
        --light: 60%;
        --strong: 40%;
        --stronger: 20%;
        --strongest: 0%;
    }
}

@media (prefers-color-scheme: dark) {
    :root {
        --lightest: 0%;
        --lighter: 20%;
        --light: 40%;
        --strong: 60%;
        --stronger: 80%;
        --strongest: 100%;
    }
}

That's it! You now have a working theme switch.

If you want to make your theme switch even cooler, you could store the selected value in localStorage so that when the user selects a particular theme, that theme will also be the one loaded next time they visit the page.

The source code of the project I was building the Theme Switch for is on GitHub: nico-bachner/v4

Last updated: 6/17/2021Edit on GitHub