Utilities
ThemeProvider
A provider that applies the light theme or the dark theme and keeps the preference of the user.
Utilities
A provider that applies the light theme or the dark theme and keeps the preference of the user.
import { Button, useTheme } from 'ferry-ui'
import { Monitor, Moon, Sun } from 'lucide-react'
export default function ThemeProviderHero() {
// The provider is at the root of the app. The hook changes the preference from any component.
const { setTheme } = useTheme()
return (
<>
<Button icon={<Sun />} onClick={() => setTheme('light')}>
Light
</Button>
<Button icon={<Moon />} onClick={() => setTheme('dark')}>
Dark
</Button>
<Button icon={<Monitor />} onClick={() => setTheme('system')}>
System
</Button>
</>
)
}Mount ThemeProvider one time, at the root of the app. The provider adds the dark class to the <html> element, or removes it. It keeps the preference in localStorage.
The preference is light, dark or system. With system, the provider follows the color scheme of the device.
import type { ReactNode } from 'react'
import { ThemeProvider } from 'ferry-ui'
export function Providers({ children }: { children: ReactNode }) {
return <ThemeProvider defaultTheme="system">{children}</ThemeProvider>
}dark class, do not mount the provider. useTheme() then reads the class.React applies the theme after it loads. This is too late for the first paint. themeInitScript() returns a script that applies the stored theme before the page shows.
Put the script in the <head>. Give it the same storageKey and the same defaultTheme as the provider.
import type { ReactNode } from 'react'
import { themeInitScript } from 'ferry-ui'
export function Document({ children }: { children: ReactNode }) {
return (
// The script sets the class before hydration: tell React that the difference is correct.
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />
</head>
<body>{children}</body>
</html>
)
}In a Vite app, add the script to index.html from the Vite configuration.
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { themeInitScript } from 'ferry-ui'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
{
name: 'ferry-ui-theme-init',
transformIndexHtml: () => [{ tag: 'script', children: themeInitScript(), injectTo: 'head-prepend' }],
},
],
})useTheme() gives the preference and a function that changes it. Bind a picker to theme. For a picker in a top bar, use Theme Menu.
import { ToggleGroup, ToggleGroupItem, useTheme, type ThemePreference } from 'ferry-ui'
const isPreference = (value: string): value is ThemePreference =>
value === 'light' || value === 'dark' || value === 'system'
export function AppearanceSetting() {
const { theme, setTheme } = useTheme()
return (
<ToggleGroup
type="single"
variant="outline"
aria-label="Theme"
value={theme}
onValueChange={(next) => {
// Single mode sends "" when the user presses the active item again: keep one option.
if (isPreference(next)) setTheme(next)
}}
>
<ToggleGroupItem value="light">Light</ToggleGroupItem>
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
<ToggleGroupItem value="system">System</ToggleGroupItem>
</ToggleGroup>
)
}To keep the preference in the account of the user, control the provider. Pass theme and onThemeChange.
<ThemeProvider theme={account.theme} onThemeChange={saveTheme}>
{children}
</ThemeProvider>To keep nothing in localStorage, pass storageKey={null}.
<ThemeProvider defaultTheme="dark" storageKey={null}>
{children}
</ThemeProvider>| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
The app (everything below can call | ||
defaultTheme | "light" | "dark" | "system" | system |
Preference used when nothing is stored yet (default | ||
storageKey | string | null | ferry-ui-theme |
localStorage key the preference is persisted under (default | ||
theme | "light" | "dark" | "system" | - |
Controlled preference (pair with | ||
onThemeChange | ((theme: ThemePreference) => void) | - |
Called with the new preference whenever | ||
useTheme() returns an object with three values. With no provider above, it reads the dark class of <html>, and setTheme does nothing.
| Value | Type | Role |
|---|---|---|
theme | ThemePreference | The preference of the user. It can be system. |
resolvedTheme | ResolvedTheme | The theme that the page shows. Use it for a chart or for code colors, which cannot use tokens. |
setTheme | (theme: ThemePreference) => void | Changes the preference. With a storageKey, it also stores the preference. |
themeInitScript(storageKey, defaultTheme) returns the source of the script as a string. The two parameters are optional. A server can call this function.
| Parameter | Type | Default | Role |
|---|---|---|---|
storageKey | string | 'ferry-ui-theme' | The localStorage key that the script reads. |
defaultTheme | ThemePreference | 'system' | The preference when the storage is empty. |
| Name | Value |
|---|---|
DEFAULT_THEME_STORAGE_KEY | 'ferry-ui-theme' |
ThemePreference | 'light' | 'dark' | 'system' |
ResolvedTheme | 'light' | 'dark' |
ThemeContextValue | The type of the object that useTheme() returns. |
ThemeProviderProps | The type of the props of ThemeProvider. |