# ThemeProvider

> Owns the appearance: light, dark, high contrast or system, persisted with no flash on load.

Category: Theming · Status: stable · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/theme-provider

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.

## Import

```tsx
import { ThemeProvider, ThemeScript, Theme, useTheme } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### Reading and changing the theme

```tsx
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>
  );
}
```

### Scoped themes

Theme sets data-theme on a wrapper so a section can differ from the page.

```tsx
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.

```tsx
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

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| ThemeProvider | `defaultTheme` | `'light' \| 'dark' \| 'high-contrast' \| 'system'` | `'system'` | Used when no preference is stored. |
| ThemeProvider | `storageKey` | `string` | `'stance-theme'` | localStorage key for the preference. |
| ThemeProvider | `legacyStorageKeys` | `string[]` | `[]` | Keys read when storageKey is empty, so an existing preference survives migration. |
| ThemeProvider | `darkClass` | `boolean` | `true` | Also toggles a dark class on <html> for Tailwind’s class strategy. |
| ThemeProvider | `respectContrastPreference` | `boolean` | `true` | System mode resolves to high contrast when prefers-contrast: more. |
| ThemeProvider | `forcedTheme` | `'light' \| 'dark' \| 'high-contrast'` | None | Locks the page to one theme regardless of preference. |
| ThemeScript | `...options` | `ThemeScriptOptions & { nonce?: string }` | None | Same options as ThemeProvider. Renders the inline pre-paint script; pass a CSP nonce if needed. |
| Theme | `theme` (required) | `'light' \| 'dark' \| 'high-contrast'` | None | Theme for the subtree. Portalled overlays opened inside inherit it. |
| useTheme | `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.

## 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.

## Related

- https://stanceui.com/components/theme-switcher
