SvelteKit Theme Switch
Tags: sveltekit, svelte, javascript, webdev
Published: 2021-05-19 21:47:50


When adding **dark mode** to my new [SvelteKit](https://kit.svelte.dev) project, I ran into a few issues when creating the **theme switch**. In this guide, I would like to share the solution I came up with.

Before we begin, I'd like to note that this guide uses [TypeScript](https://www.typescriptlang.org). TypeScript (TS) is **JavaScript with _types_**[^1], so if you are using regular JavaScript (JS), you can **skip the type definitions**.

## Types

**Note:** this step is not necessary for those using JS instead of TS, and is also optional (but recommended) for those using TS

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

```ts
// src/global.d.ts
type Theme = 'system' | 'light' | 'dark'
```

What we're doing here is declaring a _global type_ called `Theme`, which we can access from anywhere in our project. This means that if we declare a variables type to be `Theme`, then we can only assign the values `'system'`, `'light'`, or `'dark'` to it.

Apart from `'system'`, you can **choose your theme values freely**. You're also **not limited to only two**, so experiment away!

The `'system'` value here is important. We want to greet the user with their preferred theme when they first visit the site. Therefore, we want the theme to [correspond to their operating system's color scheme](#theme-styles) by default.

## 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](https://svelte.dev/docs#svelte_store). To create it, we use the `writable` function provided by Svelte.

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

const theme = writable('system')

export { theme }
```

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 we respect the user's preferences.

## Theme Switch Component

We can now use the Svelte Store we created in our theme switch component.

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

<select bind:value="{$theme}">
  <option value="system">System</option>
  <option value="light">Light</option>
  <option value="dark">Dark</option>
</select>
```

There's a lot going on here, so 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()` (more info in the [writable stores documentation](https://svelte.dev/tutorial/writable-stores)). However, there is **an easier way**: using [auto-subscriptions](https://svelte.dev/tutorial/auto-subscriptions).

Since `$theme` is **_mutable_**[^2], 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. 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.

```html
<!-- 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 />
```

Here, we are dynamically loading a CSS stylesheet based on the current theme value. For example, on page load, the previous code will generate the following:

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

And if the user then changes the theme to `'light'`, the head changes accordingly:

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

## Theme Styles

The only thing that remains 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 the stylesheet link accordingly**.

### Structure

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

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

### Example

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

```css
/* light.css */
:root {
  --color-lightest: hsl(0deg, 0%, 100%);
  --color-lighter: hsl(0deg, 0%, 80%);
  --color-light: hsl(0deg, 0%, 60%);
  --color-strong: hsl(0deg, 0%, 40%);
  --color-stronger: hsl(0deg, 0%, 20%);
  --color-strongest: hsl(0deg, 0%, 0%);
}
```

### System Preferences

While `light.css` and `dark.css` are straightforward, the file `system.css` requires more attention. This is because we need to think about the user's system preferences. While the `prefers-color-scheme` media query makes accessing the user's preference a straightforward process, we need to keep in mind that the browser provides only two predefined choices, `light` and `dark`. Hence we need to style accordingly:

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

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

## Conclusion

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

If you want to further improve your theme switch, you could store the selected value in `localStorage`. Then, when the user selects a particular theme, the same theme will also be loaded the next time they visit the page.

[^1]: Types in TypeScript explicitly declare the type of a variable. TypeScript also supports the definition of [custom types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases), called _type aliases_. These can be manipulated similarly to JavaScript variables and imported from external files.
[^2]: 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.
[^3]: This guide uses CSS Custom Properties (a.k.a. CSS Variables) for theming, but the solution works with any theming method, as long as the styles are defined inside the files we are working with.