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
  • Accessibility
  • Figma Not in Figma yet (code-only, D8)
  • packages/ui/src/components/ThemeToggle

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.

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.

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react
tsx
import '@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.

tsx
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>
  )
}
tsx
import { Switch, ThemeToggle } from '@nuton/ui'

<ThemeToggle />
tsx
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

States and interactions

System (default)Follows the device. The page changes by itself when the device switches between light and dark.
Light or DarkA fixed choice. It is saved in this browser (nu.theme) and kept in step with every other toggle on the page.
DisabledSame as Segmented control. Use only if the theme is fixed by an administrator.

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.

tsx
<section data-theme="dark" style={{ background: 'var(--nu-bg-weak-50)', color: 'var(--nu-text-strong-950)' }}>
  …
</section>

Settings row

API reference

Props

Theme toggle props
PropTypeDefaultDescription
onThemeChange(theme: "light" | "dark" | "system") => void—Called after the theme changes.
storageKeystring"nu.theme"Where the choice is saved. Must match the key passed to themeInitScript().
showLabelsbooleanfalseShow the words as well as the icons. Icon-only toggles keep the words as tooltips and accessible names.
labelsPartial<{ group; light; system; dark }>—Replaces the English strings, for translation.
size"sm" | "md" | "lg""sm"Segmented control size.
disabledbooleanfalseDisables every option.

Every other Segmented control prop (className, fullWidth and so on) is passed through.

Helpers

Theme helpers
PropTypeDefaultDescription
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

KeyBehaviour
TabMoves 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 propertyFigma valuesCode
(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-theme on its container.
  • Showing a mode that is not a theme, such as text direction.

Do and don't

DoOffer System as well as Light and Dark Most people never choose. System gives them the theme their device already uses.
Don'tOffer only a dark switch A single on/off switch cannot return to System, so the person is stuck with a fixed choice.