Components Theming
ThemeProvider
StableOwns the appearance: light, dark, high contrast or system, persisted with no flash on load.
Wrap your application once in ThemeProvider and put ThemeScript in <head>. The provider resolves the preference (including the operating system’s colour-scheme and contrast settings), persists it, syncs it across tabs and applies data-theme to <html>. Use Theme to scope a theme to part of a page, and useTheme to read or change it.
Overview
Preference: system · Applied: light
import { Button, useTheme } from '@bindshq/stance-react';
export function CurrentTheme() {
const { theme, resolvedTheme, setTheme } = useTheme();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
<p style={{ margin: 0 }}>
Preference: <strong>{theme}</strong> · Applied: <strong>{resolvedTheme}</strong>
</p>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<Button size="sm" variant="outline" onClick={() => setTheme('light')}>
Light
</Button>
<Button size="sm" variant="outline" onClick={() => setTheme('dark')}>
Dark
</Button>
<Button size="sm" variant="outline" onClick={() => setTheme('high-contrast')}>
High contrast
</Button>
<Button size="sm" variant="ghost" onClick={() => setTheme('system')}>
System
</Button>
</div>
</div>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
Preference: system · Applied: light
Preference: system · Applied: light
Preference: system · Applied: light
Import
import { ThemeProvider, ThemeScript, Theme, useTheme } from '@bindshq/stance-react';Platforms
ThemeProvider has native counterparts. They share Stance tokens and intent, and use each platform’s own controls and conventions. The live previews on this page are the web implementation.
import { ThemeProvider, ThemeScript, Theme, useTheme } from '@bindshq/stance-react';Examples
Scoped themes
Theme sets data-theme on a wrapper so a section can differ from the page.
light
dark
high-contrast
import { Button, Theme } from '@bindshq/stance-react';
export function ScopedThemes() {
const themes = ['light', 'dark', 'high-contrast'] as const;
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(10rem, 1fr))',
gap: 12,
width: '100%',
}}
>
{themes.map((t) => (
<Theme
key={t}
theme={t}
style={{
padding: 16,
borderRadius: 12,
border: '1px solid var(--st-color-border-default)',
}}
>
<p style={{ margin: '0 0 8px', fontWeight: 600 }}>{t}</p>
<Button size="sm">Deploy signature</Button>
</Theme>
))}
</div>
);
}Inverted call to action
A dark band on a light page, built with a scoped theme rather than hard-coded colours.
Roll out your new signature
Deploy to every mailbox in your organisation in one step.
import { Button, Theme } from '@bindshq/stance-react';
export function InvertedSection() {
return (
<Theme theme="dark" style={{ width: '100%', padding: 24, borderRadius: 16 }}>
<h3 style={{ margin: '0 0 6px' }}>Roll out your new signature</h3>
<p style={{ margin: '0 0 16px', color: 'var(--st-color-fg-muted)' }}>
Deploy to every mailbox in your organisation in one step.
</p>
<Button shape="pill">Start rollout</Button>
</Theme>
);
}API reference
ThemeProvider
| Prop | Type | Default | Description |
|---|---|---|---|
defaultTheme | 'light' | 'dark' | 'high-contrast' | 'system' | 'system' | Used when no preference is stored. |
storageKey | string | 'stance-theme' | localStorage key for the preference. |
legacyStorageKeys | string[] | [] | Keys read when storageKey is empty, so an existing preference survives migration. |
darkClass | boolean | true | Also toggles a dark class on <html> for Tailwind’s class strategy. |
respectContrastPreference | boolean | true | System mode resolves to high contrast when prefers-contrast: more. |
forcedTheme | 'light' | 'dark' | 'high-contrast' | None | Locks the page to one theme regardless of preference. |
ThemeScript
| Prop | Type | Default | Description |
|---|---|---|---|
...options | ThemeScriptOptions & { nonce?: string } | None | Same options as ThemeProvider. Renders the inline pre-paint script; pass a CSP nonce if needed. |
Theme
| Prop | Type | Default | Description |
|---|---|---|---|
theme* required | 'light' | 'dark' | 'high-contrast' | None | Theme for the subtree. Portalled overlays opened inside inherit it. |
useTheme
| Prop | Type | Default | Description |
|---|---|---|---|
returns | { theme, resolvedTheme, setTheme, themes } | None | theme is the stored preference (may be system); resolvedTheme is what is applied. |
Accessibility
- System mode honours prefers-color-scheme and prefers-contrast, so people who need more contrast get the high-contrast theme automatically.
- ThemeScript applies the theme before first paint, preventing a flash that can be uncomfortable for light-sensitive users.
- Reduced-motion preferences collapse every Stance duration to zero independently of the theme.
- Scoped themes keep text and boundaries at the same verified contrast as page-level themes.
Usage guidelines
Do
- Render ThemeScript in <head> with the same options as ThemeProvider.
- Use legacyStorageKeys or storageKey to keep a preference an app already stored.
Don’t
- Hard-code dark sections with raw colours; use a scoped Theme.
- Mount more than one ThemeProvider in the same document.