Svelte
January 17, 2025 • 2 min read

The simplest theme switch in Svelte 5 without any library

Learn how to implement a clean and efficient theme switcher in Svelte 5 using pure CSS, with no external libraries required.

The simplest theme switch in Svelte 5 without any library
Table of contents
Table of contents

I’m excited to share my discovery on managing theme switching between light and dark modes, or even following the system theme, using pure CSS without any libraries. This approach works with any JavaScript framework for the web, but since I’ve recently spent significant time working with Svelte, I’ll demonstrate it using the Svelte 5 framework with TypeScript. Let’s dive in!

Add data theme attribute to the root HTML

If you create a Svelte app, you will have a src/app.html file that contains the root HTML. In the <html> tag, you can add a data-theme attribute. Set it to “light” as the default theme, so it will look like this:

<html lang="en" data-theme="light"></html>

Define the CSS variables

In your app.css, which will be imported into the src/routes/+layout.svelte, you can define CSS variables for each data-theme attribute. It will look like this:

:root {
    --md: 768px;
    --lg: 1024px;
    --xl: 1280px;
}

[data-theme="light"] {
    --primary-color: 10, 105, 218;
    --primary-highlight-color: 221, 244, 255;
    --on-primary-color: 255, 255, 255;
    --secondary-color: 161, 64, 0;
    --bg-color: 245, 245, 247;
    --surface-color: 255, 255, 255;
    --surface-highlight-color: 249, 249, 249;
    --on-surface-color: 27, 27, 27;
    --subtitle-color: 110, 110, 115;
    --button-color: 242, 242, 242;
    --neutral-color: 215, 221, 228;
}

[data-theme="dark"] {
    --primary-color: 68, 147, 248;
    --primary-highlight-color: 19, 29, 46;
    --on-primary-color: 255, 255, 255;
    --secondary-color: 255, 181, 151;
    --bg-color: 2, 4, 10;
    --surface-color: 13, 17, 22;
    --surface-highlight-color: 21, 27, 35;
    --on-surface-color: 255, 255, 255;
    --subtitle-color: 240, 246, 252;
    --button-color: 25, 30, 36;
    --neutral-color: 43, 50, 60;
}

Prepare the shared rune

To persist the current theme at the framework level, we’re going to define a shared rune. Place this in src/lib/shared.svelte.ts. Here’s the content:

export const app = $state({
    theme: ''
})

Initialize the theme

Initially, we will read the system theme and set it as the default theme. To make this logic reusable across all components, let’s define it in a new shared file, src/state.svelte.ts. Here’s the content:

import { app } from "$lib/shared.svelte"

export class LayoutPageState {
  setTheme = (to = '') => {
    if (to === 'light') {
      app.theme = 'light'
    } else if (to === 'dark') {
      app.theme = 'dark'
    } else {
      app.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
    }

    document.documentElement.setAttribute('data-theme', app.theme)
  }
}

Import and call the setTheme function in src/+layout.svelte on mounted. Here’s how it look like:

<script lang="ts">
  import '../app.css'
  import { onMount } from 'svelte'
  import { LayoutPageState } from './state.svelte'

  let pageState = new LayoutPageState()

  let { children } = $props()

  onMount(() => pageState.setTheme())
</script>

{@render children()}

Toggle the theme

To allow users to toggle the theme via a button, let’s create a component in src/lib/components/LightSwitch.svelte and reuse the resources we created earlier. Here’s how it looks:

<script lang="ts">
  import { app } from '$lib/shared.svelte'
  import { LayoutPageState } from '../../routes/state.svelte'

  let pageState = new LayoutPageState()
  let to = $derived(app.theme === 'light' ? 'dark' : 'light')
</script>

<button onclick={() => pageState.setTheme(to)}>
  {#if app.theme === 'light'}
    <span>Dark</span>
  {:else}
    <span>Light</span>
  {/if}
</button>

And that’s it! With these steps, you should be able to toggle the theme using the LightSwitch.svelte component. Everything described above is implemented on this site and works perfectly.

Happy coding! Bye!