Handbook
Theming
The light theme, the dark theme, a theme picker, and the colors and fonts of your product.
Handbook
The light theme, the dark theme, a theme picker, and the colors and fonts of your product.
The light values of the tokens are on :root. The dark values are on .dark. ThemeProvider adds or removes the dark class on the <html> element.
A token class gets the correct value in each theme. You do not write a dark: variant.
Mount the provider one time, at the root of the app.
import type { ReactNode } from 'react'
import { ThemeProvider } from 'ferry-ui'
export function Providers({ children }: { children: ReactNode }) {
return <ThemeProvider defaultTheme="system">{children}</ThemeProvider>
}| Preference | Result |
|---|---|
light | The light theme. |
dark | The dark theme. |
system | The theme that the device uses. |
The provider keeps the preference in localStorage, with the key ferry-ui-theme. The ThemeProvider page gives its props.
useTheme() returns the preference and a function that changes it.
The page shows the light theme.
import * as React from 'react'
import { ToggleGroup, ToggleGroupItem, useTheme, type ThemePreference } from 'ferry-ui'
const isPreference = (value: string): value is ThemePreference =>
value === 'light' || value === 'dark' || value === 'system'
const subscribe = () => () => {}
export default function ThemePicker() {
const { theme, resolvedTheme, setTheme } = useTheme()
// A server does not know the stored preference: show it after the first render in the browser.
const mounted = React.useSyncExternalStore(
subscribe,
() => true,
() => false,
)
return (
<div className="flex flex-col items-center gap-3">
<ToggleGroup
type="single"
variant="outline"
aria-label="Theme"
value={mounted ? theme : ''}
onValueChange={(next) => {
// Single mode sends "" when the active item gets a click: keep one item selected.
if (isPreference(next)) setTheme(next)
}}
>
<ToggleGroupItem value="light">Light</ToggleGroupItem>
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
<ToggleGroupItem value="system">System</ToggleGroupItem>
</ToggleGroup>
<p className="text-[13px] text-foreground-light">
The page shows the <span className="text-foreground">{mounted ? resolvedTheme : 'light'}</span> theme.
</p>
</div>
)
}| Value | Type | Role |
|---|---|---|
theme | ThemePreference | The preference: light, dark or system. |
resolvedTheme | ResolvedTheme | The theme on the page: light or dark. |
setTheme | (theme: ThemePreference) => void | Changes the preference and saves it. |
Bind a picker to theme. Read resolvedTheme for code that cannot use tokens, for example a chart library.
In a top bar, use Theme Menu. It is a picker with a light, a dark and a system option.
React applies the theme after it loads. That is too late for the first paint.
themeInitScript() returns a small script that applies the stored theme before the first paint. Put this script in the <head>. Give it the same storageKey and 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 React starts: 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' }],
},
],
})Override the source variables after the import of ferry-ui. Put the light values on :root and the dark values on .dark.
@import "tailwindcss";
@import "ferry-ui/theme.css";
:root {
--primary: oklch(0.55 0.2 290);
--primary-solid: oklch(0.55 0.2 290);
--primary-solid-border: oklch(0.48 0.2 290);
--primary-bright: oklch(0.66 0.2 290);
--primary-soft: oklch(0.66 0.2 290 / 0.1);
--ring: oklch(0.55 0.2 290 / 0.75);
--brand: oklch(0.55 0.2 290);
}
.dark {
--primary: oklch(0.75 0.14 290);
--primary-bright: oklch(0.75 0.14 290);
--primary-soft: oklch(0.75 0.14 290 / 0.12);
--ring: oklch(0.75 0.14 290 / 0.7);
}The Tokens page shows each variable.
A container can also set the variables. In this demo, the second panel sets four variables.
import type { CSSProperties } from 'react'
import { Button, Checkbox, Label, MonoLabel, Switch, UsageBar } from 'ferry-ui'
// The source variables of the primary color and of the brand color, for one container.
const PARTNER_BRAND = {
'--primary-solid': 'oklch(0.55 0.2 290)',
'--primary-solid-border': 'oklch(0.48 0.2 290)',
'--ring': 'oklch(0.55 0.2 290 / 0.75)',
'--brand': 'oklch(0.55 0.2 290)',
} as CSSProperties
function Controls() {
return (
<div className="flex flex-col items-start gap-3">
<Label>
<Checkbox defaultChecked /> Send a weekly digest
</Label>
<Label>
<Switch size="sm" defaultChecked /> Email notifications
</Label>
<UsageBar value={40} tone="brand" label="Storage used" />
<Button variant="primary">Save changes</Button>
</div>
)
}
export default function BrandScope() {
return (
<div className="grid w-full gap-4 sm:grid-cols-2">
<section className="flex flex-col gap-4 rounded-lg border p-5">
<MonoLabel as="h3">Colors of the app</MonoLabel>
<Controls />
</section>
<section style={PARTNER_BRAND} className="flex flex-col gap-4 rounded-lg border p-5">
<MonoLabel as="h3">Colors of this container</MonoLabel>
<Controls />
</section>
</div>
)
}This method has two limits:
--chart-1 get their value on :root. In the container, declare again the alias of each token that you change.<body>. They keep the colors of the app..partner-area {
--brand: oklch(0.6 0.19 250);
--chart-1: var(--brand); /* alias of --brand */
--surface-100: oklch(0.985 0.005 250);
--card: var(--surface-100); /* alias of --surface-100 */
}Load your fonts. Then override the two font variables.
:root {
--ferry-ui-font-sans: "Geist", system-ui, sans-serif;
--ferry-ui-font-mono: "Geist Mono", ui-monospace, monospace;
}