API reference

useThemeSetting

stable
function useThemeSetting(options?: UseThemeSettingOptions): UseThemeSettingResult
options.storageKeystring | null

localStorage key used to persist the user override. Set to null to disable persistence (when the host app already owns theme state and only needs the system-preference subscription). Defaults to "motif:theme".

options.defaultResolved'light' | 'dark'

Theme to assume during SSR / before the first client effect runs. Browsers cannot know the user's system preference until JS runs, so the very first paint of a server-rendered page falls back to this. Defaults to "light".

Updated 3 days agoEdit on GitHubWeb & native

Description

Reads the user's persisted override from localStorage (if any), falls back to prefers-color-scheme, and subscribes to OS-level changes via matchMedia. Designed to pair with <ThemeProvider> to produce an automatic light/dark experience without per-page wiring.

Web only. The hook reads window.matchMedia and window.localStorage; on native, supply your own preference store and pass the result to <ThemeProvider active> directly.

UseThemeSettingResult

example.tsx
interface UseThemeSettingResult {
readonly mode: ThemeMode;             // 'system' | 'light' | 'dark'
readonly resolved: ResolvedTheme;     // 'light' | 'dark'
readonly set: (next: ThemeMode) => void;
}
  • mode — the user's selected mode. 'system' until they explicitly choose.
  • resolved — the concrete theme to render. Equal to mode unless mode === 'system', in which case it is the OS preference.
  • set(next) — updates the mode. Persists to localStorage when storageKey is set.

Types

example.tsx
type ThemeMode = 'system' | 'light' | 'dark';
type ResolvedTheme = 'light' | 'dark';
 
interface UseThemeSettingOptions {
readonly storageKey?: string | null;
readonly defaultResolved?: ResolvedTheme;
}

SSR behaviour

The first server-rendered HTML uses defaultResolved (default 'light') since the OS preference is unreachable from Node. Hydration runs the client effect and updates to the real value; React's hydration model treats this as a normal post-mount state change — no warning.

To avoid the brief flash on dark-preferring users, write the resolved value to a cookie at SSR time and pass it back in via defaultResolved.

Example

example.tsx
import { useThemeSetting, ThemeProvider } from 'usemotif';
import { lightTheme, darkTheme } from './theme';
 
export function App({ children }: { children: React.ReactNode }) {
const { resolved } = useThemeSetting();
return (
  <ThemeProvider themes={[lightTheme, darkTheme]} active={resolved}>
    {children}
  </ThemeProvider>
);
}

For a user-facing toggle:

example.tsx
function ThemeToggle() {
const { mode, set } = useThemeSetting();
return (
  <select value={mode} onChange={(e) => set(e.target.value as ThemeMode)}>
    <option value="system">System</option>
    <option value="light">Light</option>
    <option value="dark">Dark</option>
  </select>
);
}
  • ThemeProvider — receives resolved on its active prop.
  • Dark mode toggle — full UI recipe built on this hook.
  • useThemeName — reads the active theme; this hook writes it (indirectly, via the provider's active).