Theme

Sets color mode and theme inputs for the page or a nested subtree.

import { Theme } from "@usefragments/ui";Source

Preview

<Theme defaultMode="system">
  <div
    style={{
      padding: '16px',
      borderRadius: '8px',
      background: 'var(--fui-bg-elevated)',

Usage

Use when

  • An app needs light and dark modes
  • The user's choice should survive a reload
  • You want to honour the OS setting by default
  • One region needs its own mode, brand or neutral (a dark panel, an ink toolbar, a rebranded preview)

Avoid when

  • Recoloring one component (use its props)
  • A single one-off color swap (use a CSS variable)

Implementation notes

  • The outermost Theme owns <html>: mode, storage and inputs are written there
  • A Theme inside another Theme scopes its subtree: it renders one <div data-fui-theme> and every role inside re-derives from what it sets
  • A nested Theme that sets no mode, input, className or style renders nothing and passes the parent context through
  • Put ThemeScript in <head> with the same storageKey and inputs as the root Theme so a reload paints the stored mode first; add suppressHydrationWarning to <html>
  • useTheme reads and sets the nearest Theme's mode
  • storageKey renames the localStorage entry; storage that throws falls back to the default mode
  • Popups opened inside a scope carry it into their portal
  • Theme renders no chrome of its own; the colour-mode control is a composition: a ToggleGroup wired to useTheme (System, Light, Dark, System first), or one IconButton calling setMode in a tight toolbar
  • Without a Theme, useTheme reads and writes data-theme on <html> directly; toggleMode steps system → light → dark

Installation

pnpm add @usefragments/ui

Import styles once from @usefragments/ui/styles. See the installation guide for framework and theme setup.

Anatomy

Import Theme once, then compose its parts with dot notation.

  • Theme.useTheme

Examples

Colour Mode Control

A ToggleGroup wired to useTheme sets system, light or dark.

import { Monitor, Moon, Sun } from '@phosphor-icons/react';
import { ToggleGroup } from '@usefragments/ui';
import { useTheme, type ThemeMode } from '@usefragments/ui';

function ColourMode() {
  const { mode, setMode } = useTheme();

Colour Mode Button

One IconButton flips light and dark in a toolbar with no room to spare.

import { Moon, Sun } from '@phosphor-icons/react';
import { IconButton } from '@usefragments/ui';
import { useTheme } from '@usefragments/ui';

function ColourModeButton() {
  const { resolvedMode, setMode } = useTheme();

Nested Scope

A dark, green-branded panel inside the page; everything inside re-derives.

<Theme mode="dark" brand="#16a34a" style={{ padding: '16px' }}>
  <Button>Save</Button>
</Theme>

Ink Chrome

Primary actions in ink instead of the accent, for one toolbar.

<Theme primaryChrome="ink">
  <Button>Publish</Button>
</Theme>

API reference

Component props
Prop
Type
Default
Description
brandstring | undefinedNot setBrand colour, any CSS colour; sets --fui-seed-brand
childrenRequiredReact.ReactNodeNot setApplication content
classNamestring | undefinedNot setClass for the nested scope element; ignored at the root
dangerstring | undefinedNot setDanger colour; sets --fui-seed-danger
defaultModeThemeMode | undefinedlightdarksystemsystemDefault theme mode for uncontrolled usage
fontstring | undefinedNot setSans-serif font stack; sets --fui-font-sans
infostring | undefinedNot setInfo colour; sets --fui-seed-info
modeThemeMode | undefinedlightdarksystemNot setControlled theme mode
neutralThemeNeutral | undefinedpaperThe neutral every plane, ink and line is a lightness step of: paper or any CSS colour; sets --fui-seed-neutral (chroma capped at 0.04)
onModeChange((mode: ThemeMode) => void) | undefinedNot setCallback when mode changes
pressScalenumber | undefinedNot setScale an action takes while pressed, 1 turns it off; sets --fui-press-scale
primaryChromeThemeChrome | undefinedaccentinkaccentPrimary action chrome: the accent ramp, or ink on the page plane; sets data-chrome
radiusstring | number | undefinedNot setBase radius; a number is pixels; sets --fui-radius
scalenumber | undefined1Multiplier on every measurement; sets --fui-scale
storageKeystring | undefinedfui-themelocalStorage key for persistence. A nested Theme persists only when given one
styleReact.CSSProperties | undefinedNot setStyle for the nested scope element; ignored at the root
successstring | undefinedNot setSuccess colour; sets --fui-seed-success
ThemeScript.brandstring | undefinedNot setBrand colour, any CSS colour. Sets `--fui-seed-brand`.
ThemeScript.dangerstring | undefinedNot setDanger colour. Sets `--fui-seed-danger`.
ThemeScript.defaultModeThemeMode | undefinedlightdarksystemNot setMode used when nothing is stored, or storage is blocked (default: 'system').
ThemeScript.fontstring | undefinedNot setSans-serif font stack. Sets `--fui-font-sans`.
ThemeScript.infostring | undefinedNot setInfo colour. Sets `--fui-seed-info`.
ThemeScript.neutralThemeNeutral | undefinedNot setThe neutral every plane, ink and line is a lightness step of: `"paper"` or any CSS colour. Sets `--fui-seed-neutral`; chroma above 0.04 is capped so text keeps its contrast.
ThemeScript.noncestring | undefinedNot setContent Security Policy nonce for the inline script.
ThemeScript.pressScalenumber | undefinedNot setScale an action takes while pressed; 1 turns it off. Sets `--fui-press-scale`.
ThemeScript.primaryChromeThemeChrome | undefinedaccentinkNot setPrimary action chrome. Sets the `data-chrome` switch.
ThemeScript.radiusstring | number | undefinedNot setBase radius; a number, or a string holding only a number, is pixels. Sets `--fui-radius`.
ThemeScript.scalenumber | undefinedNot setMultiplier on every measurement (1 is the default). Sets `--fui-scale`.
ThemeScript.storageKeystring | undefinedNot setlocalStorage key the root `Theme` persists to (default: 'fui-theme').
ThemeScript.successstring | undefinedNot setSuccess colour. Sets `--fui-seed-success`.
ThemeScript.warningstring | undefinedNot setWarning colour. Sets `--fui-seed-warning`.
warningstring | undefinedNot setWarning colour; sets --fui-seed-warning

Accessibility

  • Name the colour-mode control (aria-label "Colour mode"); a flip button's label names the mode it switches to
  • System mode follows prefers-color-scheme
  • A scoped theme re-derives every ink and plane, so text holds 4.5:1 inside it
  • AppShellsiblingTheme typically wraps AppShell
  • ToggleGroupsiblingThe colour-mode control is a ToggleGroup wired to useTheme

Next steps