svelte-theme-picker Svelte Themes

Svelte Theme Picker

A customizable theme picker component for Svelte applications.

svelte-theme-picker

A customizable theme picker component for Svelte 5 applications.

Features

  • 10 built-in themes (dark and light modes)
  • Fully customizable - add your own themes
  • Persists selection to localStorage
  • Applies CSS variables to your document
  • Svelte 5 runes compatible
  • Full TypeScript support
  • Floating button or inline mode (vertical or horizontal)
  • Preview mode - temporarily apply themes without persisting
  • JSON-driven themes - define themes in JSON with production/test filtering
  • Theme catalogs - organize themes with metadata, tags, and filtering
  • Headless mode - use the store without any UI for full programmatic control
  • Performance optimized - GPU-accelerated animations, batched DOM updates
  • Accessible - ARIA labels, keyboard navigation (Escape to close)

Installation

npm install svelte-theme-picker
# or
pnpm add svelte-theme-picker
# or
yarn add svelte-theme-picker

Basic Usage

<script>
  import { ThemePicker } from 'svelte-theme-picker';
</script>

<ThemePicker />

This adds a floating theme picker button to the bottom-right of your page.

Configuration

Props

Prop Type Default Description
config ThemePickerConfig {} Configuration options
store ThemeStore undefined Custom theme store
position 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' 'bottom-right' Position of trigger button
showTrigger boolean true Show floating button (false = inline mode)
layout 'vertical' | 'horizontal' 'vertical' Layout direction for inline mode
onThemeChange (id: string, theme: Theme) => void undefined Callback when theme changes

Inline Layouts

<!-- Vertical list with names and descriptions -->
<ThemePicker showTrigger={false} layout="vertical" />

<!-- Horizontal grid of color swatches (compact) -->
<ThemePicker showTrigger={false} layout="horizontal" />

Configuration Options

interface ThemePickerConfig {
  storageKey?: string;      // localStorage key (default: 'svelte-theme-picker')
  defaultTheme?: string;    // Default theme ID (default: 'dreamy')
  themes?: Record<string, Theme>;  // Custom themes
  cssVarPrefix?: string;    // CSS variable prefix
}

SSR Support (Preventing Flash)

When using SvelteKit with SSR, themes are applied after JavaScript loads, causing a flash of unstyled content (FOUC). Use the ThemeHead component or SSR utilities to prevent this.

The easiest way to prevent theme flash in SvelteKit:

<!-- src/routes/+layout.svelte -->
<script>
  import { ThemeHead, ThemePicker, defaultThemes } from 'svelte-theme-picker';
</script>

<ThemeHead
  themes={defaultThemes}
  storageKey="my-app-theme"
  defaultTheme="dreamy"
  preloadFonts={true}
/>

<ThemePicker config={{ themes: defaultThemes, storageKey: 'my-app-theme' }} />

<slot />

The ThemeHead component:

  • Injects a blocking script that applies CSS variables before first paint
  • Adds no-transitions class to prevent transition animations during hydration
  • Optionally preloads Google Fonts for all themes

ThemeHead Props

Prop Type Default Description
themes Record<string, Theme> required All available themes
storageKey string 'svelte-theme-picker' localStorage key
defaultTheme string first theme Default theme ID
cssVarPrefix string '' CSS variable prefix
preventTransitions boolean true Prevent transition animations during hydration
preloadFonts boolean false Enable Google Fonts preloading
fontConfig FontConfig { provider: 'google' } Font preloading configuration

Using SSR Utilities Directly

For more control, use the SSR utilities to generate blocking scripts:

// src/hooks.server.ts
import { generateSSRHead } from 'svelte-theme-picker';
import { myThemes } from './themes';

export async function handle({ event, resolve }) {
  return resolve(event, {
    transformPageChunk: ({ html }) => {
      const ssrHead = generateSSRHead({
        themes: myThemes,
        storageKey: 'my-app-theme',
        defaultTheme: 'dreamy',
      });
      return html.replace('</head>', `${ssrHead}</head>`);
    }
  });
}

Removing No-Transitions Class

After hydration, remove the no-transitions class to enable animations:

<!-- src/routes/+layout.svelte -->
<script>
  import { browser } from '$app/environment';
  import { onMount } from 'svelte';

  onMount(() => {
    // Small delay to ensure hydration is complete
    setTimeout(() => {
      document.documentElement.classList.remove('no-transitions');
    }, 50);
  });
</script>

SSR Utility Functions

import {
  generateBlockingScript,  // Generate minified blocking script string
  generateSSRHead,         // Generate complete head HTML (script + styles + fonts)
  applyThemeToElement,     // Apply theme to an element (sync, for blocking scripts)
  getThemeCSS,            // Get CSS variable declarations as string
  extractFonts,           // Extract font names from a theme
  generateFontPreloadLinks, // Generate Google Fonts preload links
  themeSchema,            // CSS variable mapping (for consistency)
} from 'svelte-theme-picker';

Cross-Tab Synchronization

Enable automatic theme sync across browser tabs:

<script>
  import { createThemeStore, ThemePicker } from 'svelte-theme-picker';

  const store = createThemeStore({
    syncTabs: true,  // Enable cross-tab sync
    storageKey: 'my-app-theme',
  });
</script>

<ThemePicker store={store} />

When a user changes the theme in one tab, all other tabs will automatically update. The store listens for storage events and syncs the theme state.

To clean up listeners when done:

store.destroy(); // Removes storage event listener

External Store Synchronization

If you're using an external store and the ThemePicker doesn't update when the store changes externally, use the {#key} pattern to force a re-render:

<script>
  import { ThemePicker, createThemeStore } from 'svelte-theme-picker';

  const themeStore = createThemeStore({ /* config */ });
  let currentThemeId = $state('dreamy');

  // Subscribe to track external changes
  themeStore.subscribe((themeId) => {
    currentThemeId = themeId;
  });
</script>

<!-- Force re-render when theme changes externally -->
{#key currentThemeId}
  <ThemePicker store={themeStore} />
{/key}

Note: The ThemePicker component captures its configuration once at mount. This is intentional for performance. Use {#key} to create a new instance when props need to change.

Headless Mode (No UI)

The ThemePicker component is completely optional. You can use just the store and utilities for full programmatic control without rendering any UI. This is useful when:

  • You want to control themes from your own settings page
  • Themes are set based on user preferences from a database
  • You're building a custom theme switcher UI
  • You want to disable user theme switching entirely
<script>
  // No ThemePicker component needed!
  import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';
  import { onMount } from 'svelte';

  onMount(() => {
    // Load theme from user preferences, database, API, etc.
    const userTheme = getUserPreference() || 'dreamy';
    themeStore.setTheme(userTheme);
    applyTheme(defaultThemes[userTheme]);

    // Optionally subscribe to persist changes
    return themeStore.subscribe((themeId) => {
      saveUserPreference(themeId);
    });
  });
</script>

<!-- Your app content - no picker UI rendered -->
<slot />

You get all the benefits (localStorage persistence, CSS variable application, TypeScript types) without any visible theme picker.

Preview Mode (Temporary Themes)

Perfect for form-based theme selection where you want to preview themes without persisting them:

<script>
  import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';
  import { onMount } from 'svelte';

  let selectedTheme = 'dreamy';

  onMount(() => {
    // When the page unmounts, revert to persisted theme
    return () => {
      themeStore.revertPreview();
    };
  });

  function previewTheme(themeId: string) {
    selectedTheme = themeId;
    // Preview without persisting to localStorage
    themeStore.previewTheme(themeId);
    applyTheme(defaultThemes[themeId]);
  }

  function saveTheme() {
    // Persist the selection
    themeStore.setTheme(selectedTheme);
  }
</script>

<select onchange={(e) => previewTheme(e.target.value)}>
  {#each Object.entries(defaultThemes) as [id, theme]}
    <option value={id}>{theme.name}</option>
  {/each}
</select>

<button onclick={saveTheme}>Save Theme</button>

Store Preview Methods

interface ThemeStore {
  // ... standard methods ...

  /** Preview a theme temporarily without persisting */
  previewTheme: (themeId: string) => void;

  /** Revert from preview back to the persisted theme */
  revertPreview: () => void;

  /** Check if currently in preview mode */
  isPreviewMode: () => boolean;

  /** Get the persisted theme ID (ignores preview) */
  getPersistedThemeId: () => string;
}

Theme Catalogs

For larger applications, you may want to organize themes with metadata. Theme catalogs support:

  • Active/inactive - Hide themes from production while keeping them documented
  • Tags - Categorize themes (dark, light, seasonal, test, etc.)
  • Sorting - Control display order
  • Filtering - Show only relevant themes

Catalog Structure

import type { ThemeCatalog } from 'svelte-theme-picker';

const myCatalog: ThemeCatalog = {
  'brand-dark': {
    theme: { /* theme definition */ },
    meta: {
      active: true,           // Show in picker
      tags: ['dark', 'brand'],
      order: 1,               // Sort order
      seasonal: false,        // Or 'winter', 'spring', etc.
    }
  },
  'christmas': {
    theme: { /* theme definition */ },
    meta: {
      active: false,          // Hidden from picker
      tags: ['dark', 'seasonal'],
      seasonal: 'winter',
    }
  },
  'test-theme': {
    theme: { /* theme definition */ },
    meta: {
      active: false,          // Dev only
      tags: ['test'],
    }
  },
};

Filtering Themes

import {
  defaultThemeCatalog,
  filterCatalog,
  catalogToThemes,
  getActiveThemes,
  getThemesByTag,
  getThemesByAnyTag,
} from 'svelte-theme-picker';

// Get only active themes
const activeThemes = getActiveThemes(defaultThemeCatalog);

// Get themes by tag
const darkThemes = getThemesByTag(defaultThemeCatalog, 'dark');

// Get themes by any of several tags
const accentThemes = getThemesByAnyTag(defaultThemeCatalog, ['neon', 'vibrant']);

// Advanced filtering
const productionThemes = catalogToThemes(
  filterCatalog(defaultThemeCatalog, {
    activeOnly: true,
    excludeTags: ['test', 'seasonal'],
  })
);

// Use with ThemePicker
<ThemePicker config={{ themes: productionThemes }} />

JSON-Driven Themes

Define your themes in a JSON file for easy management and version control:

themes.json

{
  "brand-dark": {
    "theme": {
      "name": "Brand Dark",
      "description": "Official dark theme",
      "colors": {
        "bgDeep": "#0a0a12",
        "bgMid": "#12121a",
        "bgCard": "#1a1a24",
        "bgGlow": "#2a2a3a",
        "bgOverlay": "#0a0a12",
        "primary1": "#6366f1",
        "primary2": "#818cf8",
        "primary3": "#a5b4fc",
        "primary4": "#c7d2fe",
        "primary5": "#e0e7ff",
        "primary6": "#eef2ff",
        "accent1": "#8b5cf6",
        "accent2": "#a78bfa",
        "accent3": "#c4b5fd",
        "textPrimary": "#f8fafc",
        "textSecondary": "#cbd5e1",
        "textMuted": "#64748b"
      },
      "fonts": {
        "heading": "'Inter', sans-serif",
        "body": "'Inter', sans-serif",
        "mono": "'JetBrains Mono', monospace"
      },
      "effects": {
        "glowColor": "rgba(99, 102, 241, 0.15)",
        "glowIntensity": 0.3,
        "particleColors": ["#6366f1", "#8b5cf6"],
        "useNoise": false,
        "noiseOpacity": 0
      }
    },
    "meta": {
      "active": true,
      "tags": ["dark", "brand", "production"],
      "order": 1
    }
  },
  "christmas-special": {
    "theme": {
      "name": "Christmas",
      "description": "Festive holiday theme"
    },
    "meta": {
      "active": false,
      "tags": ["dark", "seasonal", "holiday"],
      "seasonal": "winter"
    }
  },
  "dev-test": {
    "theme": {
      "name": "Dev Test",
      "description": "Testing theme - not for production"
    },
    "meta": {
      "active": false,
      "tags": ["test", "dev"]
    }
  }
}

Loading and filtering for production:

import {
  loadCatalogFromJSON,
  getActiveThemes,
  filterCatalog,
  catalogToThemes,
} from 'svelte-theme-picker';

// Load themes from JSON (e.g., fetched or imported)
import themesJson from './themes.json';
const catalog = loadCatalogFromJSON(themesJson);

// Get only production-ready themes (active: true)
const productionThemes = getActiveThemes(catalog);

// Or with more advanced filtering
const productionThemes = catalogToThemes(
  filterCatalog(catalog, {
    activeOnly: true,
    excludeTags: ['test', 'seasonal'],
  })
);

// Use with ThemePicker
<ThemePicker config={{ themes: productionThemes }} />

This pattern lets you:

  • Document all themes in one place (production, test, seasonal)
  • Control which themes are active in production via the active flag
  • Filter by tags for specific use cases (e.g., only dark themes)
  • Version control your theme definitions alongside your code

Catalog Utilities

// Convert simple themes to catalog
import { themesToCatalog } from 'svelte-theme-picker';
const catalog = themesToCatalog(myThemes, { active: true });

// Merge catalogs (combine default themes with your custom ones)
import { mergeCatalogs, defaultThemeCatalog } from 'svelte-theme-picker';
const combined = mergeCatalogs(defaultThemeCatalog, myCatalog);

// Get all tags in a catalog
import { getCatalogTags } from 'svelte-theme-picker';
const tags = getCatalogTags(myCatalog); // ['dark', 'brand', 'seasonal', ...]

// Sort catalog by order
import { sortCatalog } from 'svelte-theme-picker';
const sorted = sortCatalog(myCatalog);

Custom Theme Picker UI

For complete control over the theme picker UI (e.g., in forms), you can build your own using the exported utilities. This is useful when you need color swatches in a specific layout or want to match your app's design system.

Color Swatch Grid (Form Integration)

<script lang="ts">
  import {
    themeStore,
    applyTheme,
    defaultThemes,
    type Theme
  } from 'svelte-theme-picker';
  import { onMount } from 'svelte';

  let selectedTheme = $state('dreamy');

  // Revert to saved theme when leaving the page
  onMount(() => {
    return () => themeStore.revertPreview();
  });

  function previewTheme(themeId: string) {
    selectedTheme = themeId;
    themeStore.previewTheme(themeId);
    applyTheme(defaultThemes[themeId]);
  }

  function saveSelection() {
    // Persist the theme when form is submitted
    themeStore.setTheme(selectedTheme);
  }
</script>

<div class="theme-grid">
  {#each Object.entries(defaultThemes) as [id, theme]}
    <button
      type="button"
      class="theme-swatch"
      class:selected={selectedTheme === id}
      onclick={() => previewTheme(id)}
      title={theme.name}
    >
      <div class="colors">
        <span style="background: {theme.colors.primary1}"></span>
        <span style="background: {theme.colors.primary3}"></span>
        <span style="background: {theme.colors.primary5}"></span>
        <span style="background: {theme.colors.accent1}"></span>
      </div>
      <span class="name">{theme.name}</span>
    </button>
  {/each}
</div>

<style>
  .theme-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }

  .theme-swatch {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 8px;
    background: transparent;
    border: 2px solid transparent;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s;
  }

  .theme-swatch:hover {
    background: rgba(255, 255, 255, 0.05);
  }

  .theme-swatch.selected {
    border-color: var(--accent-1, #a855f7);
    background: rgba(168, 85, 247, 0.1);
  }

  .colors {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 2px;
    width: 32px;
    height: 32px;
    border-radius: 6px;
    overflow: hidden;
  }

  .colors span {
    width: 100%;
    height: 100%;
  }

  .name {
    font-size: 0.75rem;
    color: var(--text-secondary, #a0a0a0);
  }

  .theme-swatch.selected .name {
    color: var(--text-primary, #fff);
  }
</style>

Using with Forms

When integrating theme selection in a form (e.g., collection creation), use the preview pattern:

  1. On mount: Save the current theme with getPersistedThemeId()
  2. On selection: Use previewTheme() + applyTheme() for live preview
  3. On cancel/leave: Call revertPreview() to restore the original theme
  4. On save: Call setTheme() to persist the selection
<script>
  import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';
  import { onMount } from 'svelte';

  let selectedTheme = 'dreamy';
  let formSubmitted = false;

  onMount(() => {
    // Restore original theme if user leaves without saving
    return () => {
      if (!formSubmitted) {
        themeStore.revertPreview();
      }
    };
  });

  function selectTheme(themeId) {
    selectedTheme = themeId;
    themeStore.previewTheme(themeId);
    applyTheme(defaultThemes[themeId]);
  }

  function handleSubmit() {
    formSubmitted = true;
    themeStore.setTheme(selectedTheme);
    // ... save to database
  }
</script>

Custom Themes

You can provide your own themes:

<script>
  import { ThemePicker, type Theme } from 'svelte-theme-picker';

  const myThemes: Record<string, Theme> = {
    'my-theme': {
      name: 'My Theme',
      description: 'A custom theme',
      colors: {
        bgDeep: '#1a1a2e',
        bgMid: '#232342',
        bgCard: '#2a2a4a',
        bgGlow: '#3d3d6b',
        bgOverlay: '#1a1a2e',
        primary1: '#c9a0dc',
        primary2: '#b8a9d9',
        primary3: '#e8a4c9',
        primary4: '#7eb8da',
        primary5: '#8ad4d4',
        primary6: '#f0c4a8',
        accent1: '#a855f7',
        accent2: '#ff6b9d',
        accent3: '#64c8ff',
        textPrimary: '#e8e0f0',
        textSecondary: '#c8c0d8',
        textMuted: '#9090b0',
      },
      fonts: {
        heading: "'Inter', sans-serif",
        body: "'Inter', sans-serif",
        mono: "'JetBrains Mono', monospace",
      },
      effects: {
        glowColor: 'rgba(168, 85, 247, 0.15)',
        glowIntensity: 0.3,
        particleColors: ['#c9a0dc', '#a855f7'],
        useNoise: false,
        noiseOpacity: 0,
      },
    },
  };
</script>

<ThemePicker config={{ themes: myThemes, defaultTheme: 'my-theme' }} />

Styling the Picker

The ThemePicker component can be customized using CSS custom properties. The picker automatically uses your theme's CSS variables as fallbacks, so it adapts to your theme.

Picker CSS Properties

Override these to customize the picker appearance:

/* In your global CSS or :root */
:root {
  /* Colors */
  --stp-bg: var(--bg-card);           /* Panel background */
  --stp-bg-hover: var(--bg-glow);     /* Hover state background */
  --stp-bg-active: ...;               /* Active/selected item */
  --stp-border: ...;                  /* Border color */
  --stp-text: var(--text-primary);    /* Primary text */
  --stp-text-muted: var(--text-muted);/* Secondary text */
  --stp-accent: var(--accent-1);      /* Accent color */
  --stp-accent-glow: ...;             /* Glow effect color */

  /* Trigger button */
  --stp-trigger-bg: ...;              /* Trigger background gradient */
  --stp-trigger-color: var(--bg-deep);/* Trigger icon color */

  /* Layout */
  --stp-radius: 12px;                 /* Panel border radius */
  --stp-radius-sm: 8px;               /* Item border radius */
  --stp-space: 12px;                  /* Standard spacing */
  --stp-space-sm: 8px;                /* Small spacing */
  --stp-transition: 0.2s ease;        /* Transition timing */
}

The picker uses color-mix() for calculated colors (active states, borders) that automatically adjust to your theme. No !important overrides should be needed.

Theme Mode

Themes can declare a mode property ('light' or 'dark') to help the picker adjust its styling:

const myTheme: Theme = {
  name: 'My Light Theme',
  description: 'A light theme',
  mode: 'light',  // or 'dark'
  colors: { ... },
  fonts: { ... },
  effects: { ... },
};

CSS Variables

The theme picker applies these CSS variables to your document:

Background Colors

  • --bg-deep - Deepest background
  • --bg-mid - Mid-level background
  • --bg-card - Card/surface background
  • --bg-glow - Glow/highlight background
  • --bg-overlay - Overlay background

Primary Colors (1-6)

  • --primary-1 through --primary-6

Accent Colors (1-3)

  • --accent-1 through --accent-3

Text Colors

  • --text-primary
  • --text-secondary
  • --text-muted

Fonts

  • --font-heading
  • --font-body
  • --font-mono

Effects

  • --shadow-glow
  • --glow-color
  • --glow-intensity

Using the Store Directly

<script>
  import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';

  // Subscribe to changes
  $effect(() => {
    const theme = defaultThemes[$themeStore];
    if (theme) {
      console.log('Theme changed to:', theme.name);
    }
  });

  // Change theme programmatically
  function setDarkMode() {
    themeStore.setTheme('mono');
  }
</script>

Built-in Themes

ID Name Tags Description
dreamy Dreamy dark, pastel Soft pastels with dreamy atmosphere
cyberpunk Cyberpunk dark, neon High contrast neons
sunset Sunset dark, warm Warm oranges and purples
ocean Ocean dark, cool Deep blues and teals
mono Mono dark, minimal Clean monochromatic
sakura Sakura dark, pastel Cherry blossom pinks
aurora Aurora dark, nature Northern lights greens and purples
galaxy Galaxy dark, space Deep space cosmic colors
milk Milk light, neutral Clean, creamy light theme
light Light light, modern Modern light theme with purple accents

Performance

The theme picker is optimized to minimize impact on your application:

  • CSS Containment: Uses contain: layout style to isolate rendering
  • GPU Acceleration: Animations use transform and will-change for smooth 60fps transitions
  • Batched DOM Updates: Theme changes use requestAnimationFrame to batch all CSS variable updates into a single paint frame
  • Efficient Transitions: Only animates specific properties instead of transition: all
  • Minimal Bundle: ~75KB total including all 10 built-in themes

Accessibility

The component follows accessibility best practices:

  • Keyboard Navigation: Press Escape to close the theme panel
  • ARIA Labels: All interactive elements have proper aria-label attributes
  • Role Attributes: Theme list uses role="listbox" with role="option" items
  • Focus Management: Proper aria-expanded and aria-haspopup on trigger button
  • Screen Reader Support: Decorative elements marked with aria-hidden="true"

Browser Support

  • Modern browsers (Chrome, Firefox, Safari, Edge)
  • Safari: Full support including -webkit-backdrop-filter for blur effects
  • Graceful degradation: localStorage errors are handled silently (e.g., private browsing)

TypeScript

Full TypeScript support with exported types:

import type {
  // Theme types
  Theme,
  ThemeColors,
  ThemeFonts,
  ThemeEffects,
  ThemePickerConfig,
  // Catalog types
  ThemeCatalog,
  ThemeCatalogEntry,
  ThemeMeta,
  ThemeFilterOptions,
  // SSR types
  SSRConfig,
  FontConfig,
  ThemeSchema,
} from 'svelte-theme-picker';

Migration Guide

Upgrading from a previous version? See MIGRATION.md for breaking changes and migration steps.

License

MIT

Top categories

Loading Svelte Themes