Building a Theme Switcher UI in Svelte

Introduction to Theme Switching UI

Welcome to the fifth lesson of our Advanced UI Features and Theming course! In our previous lesson, we built a comprehensive theme system that allows our Kanban application to support light, dark, and sepia themes. We created a theme configuration, implemented state management with Svelte runes, and set up theme persistence using localStorage.

However, our users currently have no way to interact with this theme system. While we've implemented the underlying functionality, we need to create a user interface that allows users to switch between themes easily. That's what we'll focus on in this lesson.

We'll build two main components:

  1. ThemeToggle: A button that, when clicked, displays a dropdown menu with available themes. This component will allow users to select their preferred theme.

  2. Header: A component that displays the application title and the current theme, along with the ThemeToggle component.

These components will work with our existing theme system, using the activeTheme state and setTheme function we created in the previous lesson. By the end of this lesson, users will be able to switch between themes with a simple click, and they'll receive visual feedback about the current theme.

Let's start by building the ThemeToggle component, which will be the primary interface for theme switching.

Building the ThemeToggle Component

The ThemeToggle component will be a button that, when clicked, displays a dropdown menu with available themes. Each theme option in the dropdown will show a color sample and the theme name. When a theme is selected, the dropdown will close, and the theme will be applied.

Let's create a new file called ThemeToggle.svelte in the src/components directory:

<script>
  import { getActiveTheme, setTheme, themes } from '$lib/themeStore.svelte.js';
  import { slide } from 'svelte/transition';
  
  let showThemeMenu = $state(false);
  
  function toggleThemeMenu() {
    showThemeMenu = !showThemeMenu;
  }
  
  function selectTheme(themeName) {
    setTheme(themeName);
    showThemeMenu = false;
  }
  
  // Get theme icon based on current theme
  const themeIcon = $derived(() => {
    switch (getActiveTheme()) {
      case 'light': return '☀️';
      case 'dark': return '🌙';
      case 'sepia': return '📜';
      default: return '☀️';
    }
  });
</script>

<div class="theme-toggle">
  <button class="theme-button" onclick={toggleThemeMenu} aria-label="Change Theme">
    <span class="icon">{themeIcon()}</span>
  </button>
  
  {#if showThemeMenu}
    <div 
      class="theme-menu" 
      in:slide|local={{ duration: 150, y: -5 }}
      out:slide|local={{ duration: 100, y: -5 }}
    >
      {#each Object.keys(themes) as themeName}
        <button 
          class="theme-option {getActiveTheme() === themeName ? 'active' : ''}"
          onclick={() => selectTheme(themeName)}
        >
          <span class="theme-color" style="background: var(--color-columnBg);"></span>
          <span class="theme-name">{themeName}</span>
        </button>
      {/each}
    </div>
  {/if}
</div>

<style>
  .theme-toggle {
    position: relative;
  }
  
  .theme-button {
    background: var(--color-muted);
    color: var(--color-text);
    border: none;
    border-radius: 9999px;
    width: 2.5rem;
    height: 2.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background-color 0.2s;
  }
  
  .theme-button:hover {
    background: var(--color-primary);
    color: white;
  }
  
  .icon {
    font-size: 1.2rem;
  }
  
  .theme-menu {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 0.5rem;
    background: var(--color-cardBg);
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    border-radius: 0.5rem;
    padding: 0.5rem;
    min-width: 150px;
    z-index: 10;
  }
  
  .theme-option {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem;
    background: transparent;
    border: none;
    border-radius: 0.25rem;
    width: 100%;
    text-align: left;
    color: var(--color-text);
    cursor: pointer;
    transition: background-color 0.2s;
  }
  
  .theme-option:hover {
    background: var(--color-muted);
  }
  
  .theme-option.active {
    background: var(--color-primary);
    color: white;
  }
  
  .theme-color {
    display: block;
    width: 1rem;
    height: 1rem;
    border-radius: 9999px;
    border: 1px solid var(--color-secondary);
  }
  
  .theme-name {
    font-size: 0.875rem;
    text-transform: capitalize;
  }
</style>

Let's break down this component:

In the script section, we import the necessary functions and state from our theme store: activeTheme, setTheme, and themes. We also import the slide transition from Svelte for smooth animations.

We create a local state variable, showThemeMenu, using the $state rune to track whether the dropdown menu is open or closed. The toggleThemeMenu function toggles this state when the button is clicked.

The selectTheme function calls the setTheme function from our theme store with the selected theme name and closes the dropdown menu.

We use the $derived rune to create a reactive variable, themeIcon, that returns an appropriate emoji based on the current theme: sun for light, moon for dark, and scroll for sepia.

In the template section, we create a button with the theme icon. When clicked, this button toggles the dropdown menu. If the menu is open, we display a list of theme options using the #each directive to iterate over the keys of the themes object.

Each theme option is a button that, when clicked, calls the selectTheme function with the theme name. The button includes a color sample using the --color-columnBg CSS variable, which will reflect the color of that theme, and the theme name. The currently active theme is highlighted with an active class.

We use the slide transition to animate the dropdown menu when it appears and disappears, creating a smoother user experience.

In the style section, we define the appearance of the theme toggle button and dropdown menu. The button is a circular button with the theme icon, and the dropdown menu is a card with a list of theme options. We use CSS variables from our theme system to ensure that the component adapts to the current theme.

Now that we have our ThemeToggle component, let's create the Header component that will display the application title and the current theme.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal