Theme toggle
Theme toggle switches the interface between Light, System and Dark. Use it in a header or settings so people can choose how Nuton looks.
Beta until design approves the dark palette (code-only, C69) and Figma gets a Dark mode on the Mapped collection (FC-38). The control itself is a Segmented control.
Preview
The toggle on this page is live: it changes the whole documentation site, and so does the one in the header.
<ThemeToggle />
<ThemeToggle showLabels />Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
Two pieces make a theme work: the script that sets the theme before the page paints, and the toggle that changes it.
import { themeInitScript } from '@nuton/ui'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-theme="system" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />
</head>
<body>{children}</body>
</html>
)
}import { Switch, ThemeToggle } from '@nuton/ui'
<ThemeToggle />import { useTheme } from '@nuton/ui'
const { theme, resolvedTheme, setTheme } = useTheme()
// theme: 'light' | 'dark' | 'system' resolvedTheme: 'light' | 'dark' (what is showing)- Always add
themeInitScript()to<head>. Without it the page paints light first and flashes dark on load. - Default to System. People who have not chosen get what their device is set to.
- Put one toggle where people can always reach it, such as the header or account settings.
- Use
data-theme="dark"on any element to make just that area dark, whatever the page theme is.
Variants and sizes
Theme toggle is a three-option Segmented control. It takes the Segmented control sizes.
Sizes
<ThemeToggle size="sm" />
<ThemeToggle size="md" />
<ThemeToggle size="md" showLabels />States and interactions
nu.theme) and kept in step with every other toggle on the page.Composition examples
An area that is always dark
Set data-theme="dark" on an element and use ordinary tokens inside it. The docs code blocks, the review bar and the dark bands on the website pages work this way.
<section data-theme="dark" style={{ background: 'var(--nu-bg-weak-50)', color: 'var(--nu-text-strong-950)' }}>
…
</section>Settings row
API reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
onThemeChange | (theme: "light" | "dark" | "system") => void | — | Called after the theme changes. |
storageKey | string | "nu.theme" | Where the choice is saved. Must match the key passed to themeInitScript(). |
showLabels | boolean | false | Show the words as well as the icons. Icon-only toggles keep the words as tooltips and accessible names. |
labels | Partial<{ group; light; system; dark }> | — | Replaces the English strings, for translation. |
size | "sm" | "md" | "lg" | "sm" | Segmented control size. |
disabled | boolean | false | Disables every option. |
Every other Segmented control prop (className, fullWidth and so on) is passed through.
Helpers
| Prop | Type | Default | Description |
|---|---|---|---|
themeInitScript(storageKey?) | string | — | Inline script for <head>. Sets data-theme on <html> from the saved choice, or "system". |
useTheme(storageKey?) | { theme, resolvedTheme, setTheme } | — | Reads and sets the theme. resolvedTheme turns "system" into what the device shows. |
applyTheme(mode, storageKey?) | void | — | Sets data-theme, saves the choice and tells every toggle on the page. |
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves focus into the group (to the selected option). |
| ← / → | Moves to the previous or next theme and selects it. |
Screen readers
A radio group named "Theme". Each option is named Light, System or Dark even when only icons show. Translate with labels.
Focus
The Segmented control focus ring on the focused option. The ring is drawn in both themes.
Contrast and colour
Dark colours are checked in npm run tokens:check against the same 4.5:1 text and 3:1 non-text rules as light. The selected option is not shown by colour alone: it also lifts on a surface.
Touch and pointer
Each option is at least 24 × 24, as in Segmented control.
You must: add themeInitScript() to <head>, and check any area you colour yourself in dark, because raw colours do not change with the theme.
Figma mapping
| Figma property | Figma values | Code |
|---|---|---|
| (none) | — | Theme toggle is code-only (D8). Figma has one colour mode; the dark values are generated from it (C69) until a Dark mode is added (FC-38). |
Guidelines and related components
Use Theme toggle for
- Choosing Light, System or Dark for the whole interface.
- Account or appearance settings, and app headers.
Don't use Theme toggle for
- Switching one component's look. Use that component's appearance prop, or
data-themeon its container. - Showing a mode that is not a theme, such as text direction.