ThemeSwitcher
A labeled control for choosing between light, dark, and system appearance. Each option is a preview card depicting that mode, and selecting one drives the global <html data-theme> through useTheme, so a bare <ThemeSwitcher /> switches the whole app with no wiring.
<script setup lang="ts">
import { ThemeSwitcher } from 'frappe-ui'
</script>
<template>
<ThemeSwitcher />
</template>Branding
Pass a name and a logo (an image URL or a Component) to show your branding inside the theme panel previews. The label and description props set the heading of the component.
<script setup lang="ts">
import { ThemeSwitcher } from 'frappe-ui'
</script>
<template>
<ThemeSwitcher
name="CRM"
logo="https://raw.githubusercontent.com/frappe/crm/develop/.github/logo.svg"
label="Appearance"
description="Pick how your branding looks on different theme modes"
/>
</template>Toggle button
In a header or sidebar you rarely want the full card preview. You want a single labelled button that flips the state. Build one from the same useTheme composable: because both controls share the one <html data-theme>, they stay in sync, and the button drops straight into a sidebar or menu.
<script setup lang="ts">
import { Button, useTheme } from 'frappe-ui'
// The full ThemeSwitcher is for settings panels; in a header, sidebar, or menu
// you usually want a single labelled button. Build it from the same `useTheme`
// composable so both controls drive the one `<html data-theme>`. Drops straight
// into a frappe-ui sidebar.
const { currentTheme, toggleTheme } = useTheme()
</script>
<template>
<Button @click="toggleTheme">
Toggle theme
<template #prefix>
<LucideSun
v-if="currentTheme === 'dark'"
class="size-4"
/>
<LucideMoon
v-else
class="size-4"
/>
</template>
</Button>
</template>In a user menu
A user menu is the most common home for theme switching. Nest the three options as a hover submenu inside a Dropdown, drive them through the same useTheme singleton, and mark the active one with selected so the menu always reflects the shared <html data-theme>.
<script setup lang="ts">
import { computed } from 'vue'
import { Avatar, Dropdown, useTheme, type DropdownOptions } from 'frappe-ui'
const { currentTheme, setTheme } = useTheme()
const themes = [
{ label: 'Light', icon: 'lucide-sun', value: 'light' },
{ label: 'Dark', icon: 'lucide-moon', value: 'dark' },
{ label: 'System', icon: 'lucide-monitor', value: 'system' },
] as const
const menuOptions = computed<DropdownOptions>(() => [
{ icon: 'lucide-user', label: 'My Profile', onClick: () => {} },
{
icon: 'lucide-sun-moon',
label: 'Theme',
submenu: themes.map((theme) => ({
label: theme.label,
icon: theme.icon,
selected: currentTheme.value === theme.value,
onClick: () => setTheme(theme.value),
})),
},
{ icon: 'lucide-log-out', label: 'Log out', onClick: () => {} },
])
const crmLogo = 'https://raw.githubusercontent.com/frappe/crm/develop/.github/logo.svg'
</script>
<template>
<Dropdown :options="menuOptions">
<template #default="{ open }">
<button
class="flex items-center gap-2 rounded-md px-2 py-2"
:class="open ? 'bg-surface-gray-3' : 'hover:bg-surface-gray-2'"
>
<Avatar shape='square' :image="crmLogo" label="CRM" />
<div class="text-start">
<div class="text-base font-medium leading-none text-ink-gray-9">
CRM
</div>
<div class="mt-1 text-sm text-ink-gray-7">faris@example.com</div>
</div>
<LucideChevronDown class="ml-2 size-4 text-ink-gray-5" />
</button>
</template>
<template #item-suffix="{ selected }">
<LucideCheck v-if="selected" class="size-4 text-ink-gray-7" />
</template>
</Dropdown>
</template>useTheme
The component is backed by the useTheme composable, exported from the library. Its state is a shared singleton, so every consumer (the switcher, a sidebar toggle, a user-menu entry) stays in sync with the single <html data-theme> source of truth.
There is nothing to set up. The first useTheme() call restores the saved theme (falling back to system) and starts following the OS preference. Calling it near your app root simply makes that happen as early as possible.
import { useTheme } from 'frappe-ui'
const { currentTheme, setTheme, toggleTheme } = useTheme()The selection persists to localStorage under the theme key and is reapplied on the next load.
| Member | Type | Description |
|---|---|---|
currentTheme | Ref<Theme> | The selected theme: 'light' | 'dark' | 'system'. |
setTheme | (theme: Theme) => void | Sets the theme, applies data-theme, and persists to storage. |
toggleTheme | () => void | Flips between light and dark. |
initializeTheme | () => void | Restores the saved theme (or system). Run once automatically. |
getSystemTheme | () => 'light' | 'dark' | Resolves the current OS preference. |
Avoid the flash. The theme is applied from JavaScript once the app loads, so a page that ships without a
data-themebriefly shows the default theme before switching. Set an initialdata-themeon your<html>, or inline a small script that readslocalStorage.theme, to render the right theme from the first paint.
API Reference
Show types
import type { Component } from 'vue'
import type { Theme } from '../../utils/theme'
export interface ThemeSwitcherProps {
/** Selected theme. Falls back to the shared `useTheme` state when unbound. */
modelValue?: Theme
/**
* Heading rendered above the options.
* @default "Theme"
*/
label?: string
/**
* Helper text rendered below the heading.
* @default "Switch between light, dark, or system theme"
*/
description?: string
/**
* Brand logo shown inside each preview. A string is treated as an image
* source; a component value is rendered with `<component :is>`.
*/
logo?: string | Component
/** Brand name shown inside each preview. */
name?: string
/**
* Overrides the per-option labels. For richer per-item content, use the
* `#item-label` slot, which takes precedence over this prop.
* @default { light: "Light", dark: "Dark", system: "System" }
*/
themeLabels?: Partial<Record<Theme, string>>
}Selected theme. Falls back to the shared `useTheme` state when unbound.
Heading rendered above the options.
Helper text rendered below the heading.
Brand logo shown inside each preview. A string is treated as an image source; a component value is rendered with `<component :is>`.
Brand name shown inside each preview.
Overrides the per-option labels. For richer per-item content, use the `#item-label` slot, which takes precedence over this prop.
| Slot | Payload |
|---|---|
label | — Overrides the heading content. |
description | — Overrides the helper-text content. |
item-label | { value: Theme; } Overrides a single option's label. Receives the option's `value`. Falls back to the `themeLabels` prop, then the built-in label. |
Overrides the heading content.
Overrides the helper-text content.
Overrides a single option's label. Receives the option's `value`. Falls back to the `themeLabels` prop, then the built-in label.
| Event | Payload |
|---|---|
update:modelValue | [theme: Theme] Fired when the model value changes. |
Fired when the model value changes.