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
}
}
mode | Tokens loaded | color-scheme | Switcher | Typical use |
|---|---|---|---|---|
'system' | light + dark | dark in dark contexts (follows OS) | ❌ none | Follow the OS, but don't let users toggle manually |
'toggle' | light + dark | dark in dark contexts | ✅ useTheme() + SwThemeSwitch | Users switch light/dark and it is remembered |
'dark' | fixed dark | dark | ❌ | Fixed dark site (palette provided by applier overrides) |
'light' (starter default) | fixed light | light | ❌ | 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/lightdeclarecolor-schemepermanently on:root;system/toggledeclarecolor-scheme: darkonly 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>
themeis the user-chosen state ('auto' | 'light' | 'dark');resolvedThemeis the actual current light/dark ('light' | 'dark', withautoresolving to the system preference) — switch logos / illustrations offresolvedTheme.toggleTheme()/setTheme()only take effect inmode: '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>
sizeprop: 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 }(#autoonly 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).