Theme System

Theming is driven by the single theme.mode knob in swift.config.json — one field decides three things at once: which token set to load, which color-scheme to declare, and whether a switcher is provided.

theme.mode — pick one of four

// nuxt.config.ts
swiftConfig: {
  theme: {
    mode: 'toggle',      // 'system' | 'toggle' | 'dark' | 'light'
    default: 'auto',     // toggle only: switcher initial state 'auto' | 'light' | 'dark', default 'auto'
    toggleStates: 3,     // toggle only: 3 = tri-state (can return to auto) / 2 = two-state (light/dark only), default 3
  }
}
modeTokens loadedcolor-schemeSwitcherTypical use
'system'light + darkdark in dark contexts (follows OS)❌ noneFollow the OS, but don't let users toggle manually
'toggle'light + darkdark in dark contexts✅ useTheme() + SwThemeSwitchUsers switch light/dark and it is remembered
'dark'fixed darkdark❌Fixed dark site (palette provided by applier overrides)
'light' (starter default)fixed lightlight❌Fixed light site — most company projects

theme only accepts the object form. For a fixed single-theme site, write { mode: 'light' } or { mode: 'dark' } directly.

Every mode keeps native UI in sync (scrollbars, dropdowns, date pickers, etc.), avoiding jarring cases like "a dark page with light scrollbars" — but the declaration differs by mode. Fixed dark / light declare color-scheme permanently on :root; system / toggle declare color-scheme: dark only in dark contexts (@media (prefers-color-scheme: dark) and [data-theme='dark']), leaving light contexts undeclared so the browser default (effectively light) applies.

How it works

In toggle mode, the framework sets data-theme="light|dark" on <html>, and CSS overrides the variables via attribute selectors. The framework also injects an inline bootstrap script into <head> automatically: it synchronously reads the user preference from localStorage before the first paint and sets data-theme early, preventing a flash of the wrong theme (FOUC) when the saved choice differs from the system preference — no configuration needed. Variable override example:

:root {
  --background: #ffffff;
  --foreground: #1a1a1a;
}

[data-theme='dark'] {
  --background: #0f0f12;
  --foreground: #e5e7eb;
}

auto (follow-system) uses the prefers-color-scheme media query to detect the OS preference and switches automatically when it changes. Fixed dark / light mode instead injects the corresponding color-scheme declaration and mounts no data-theme switching logic.

Using useTheme()

<script setup lang="ts">
const { theme, resolvedTheme, mode, toggleTheme, setTheme } = useTheme()
</script>

<template>
  <p>Current theme: {{ theme }} (actual light/dark: {{ resolvedTheme }})</p>

  <!-- toggleTheme / setTheme only take effect in toggle mode; no-op otherwise -->
  <button @click="toggleTheme()">Toggle theme</button>
  <button @click="setTheme('dark')">Dark</button>
  <button @click="setTheme('light')">Light</button>
  <button @click="setTheme('auto')">Follow system</button>
</template>
  • theme is the user-chosen state ('auto' | 'light' | 'dark'); resolvedTheme is the actual current light/dark ('light' | 'dark', with auto resolving to the system preference) — switch logos / illustrations off resolvedTheme.
  • toggleTheme() / setTheme() only take effect in mode: 'toggle'; in other modes they are no-ops (the theme is locked by the mode).

For the full API see the useTheme() composable docs.

Built-in switcher SwThemeSwitch

When you don't want to write your own switch button, drop in the framework's built-in SwThemeSwitch — it is driven by useTheme() and self-gates: it renders a button only in mode: 'toggle' and renders nothing otherwise. Change the theme.mode config and the switcher appears / disappears automatically, with no template changes.

<template>
  <!-- Default inline SVG icons (sun / moon / auto half-circle); auto-hidden when mode ≠ toggle -->
  <SwThemeSwitch />
  <SwThemeSwitch :size="22" />

  <!-- The framework binds no icon library: use named slots to plug in your own icons -->
  <SwThemeSwitch>
    <template #light><PhSun :size="18" /></template>
    <template #dark><PhMoon :size="18" /></template>
    <template #auto><PhCircleHalf :size="18" /></template>
  </SwThemeSwitch>
</template>
  • size prop: size of the default inline icons (px, default 18; ignored when you pass custom icons via slots).
  • slots #light / #dark / #auto: override the icon for each state; the slot scope provides { theme, resolved } (#auto only appears in tri-state).
  • Skinned via --sw-theme-switch-* tokens (size / radius / hover background, etc.); appliers can override them.

Persistence

The user's choice is stored under the theme key in localStorage and restored automatically on refresh. To read it synchronously during the first-paint inline script and avoid FOUC, useTheme() uses raw localStorage (not the encrypted cache util) — this is a framework-sanctioned exception; when doing theme switching in business code, share the same key with it.

Custom theme variables

/* Override framework defaults in assets/styles/global.css */
:root {
  --primary: #6366f1;
}

[data-theme='dark'] {
  --primary: #818cf8;
}

In fixed dark mode, just define the dark palette directly in :root (no [data-theme] selector needed, since there is no switching).