useTheme
Read the painted theme, the user's preference, the mutator, and the transition flag from the nearest provider.
useTheme returns the painted theme, the user's preference, the
setTheme mutator, and an isTransitioning flag. It throws if
called outside a ThemeTransitionProvider.
Reference
const { theme, preference, setTheme, isTransitioning } = useTheme()The hook is created by createThemeTransition
and is fully typed against your theme map. theme.name,
theme.colors, and setTheme's argument all narrow to the names
you defined in themes.
Returns
| Field | Type | Description |
|---|---|---|
theme.name | Names | The theme currently painted on screen. Always concrete. Never 'system'. |
theme.colors | Record<Token, string> | Resolved color tokens for the painted theme. |
theme.scheme | 'light' | 'dark' | Binary classification, derived from darkThemes. |
preference | Names | 'system' | What the user explicitly picked. Mirrors the last value passed to setTheme. |
isTransitioning | boolean | true while a transition overlay is visible. |
setTheme | (name, opts?) => 'accepted' | 'ignored' | Change the user's preference. See setTheme below. |
Theme vs Preference
The hook exposes two related but distinct values, and choosing the right one matters.
themedescribes what is currently painted on screen.theme.nameis always concrete (never'system'). When the user picks'system', the library resolves it via systemThemeMap and exposes the result here.preferencedescribes what the user explicitly picked. It can be'system'. It mirrors the last argument passed tosetTheme.
Decision Table
| You want to | Read | Why |
|---|---|---|
| Style a component | theme.colors.* | Real values, resolved. |
StatusBar content style and isDark checks | theme.scheme | Binary, regardless of theme count. |
| Toggle dark to light | theme.name | Always concrete, works in system mode. |
Per-theme asset (e.g. logos[name]) | theme.name | Always a real theme key. |
| Analytics for what was rendered | theme.name | The painted theme. |
| Theme picker highlight | preference | Must be able to show 'system' as selected. |
| Analytics for what the user picked | preference | Carries intent, including 'system'. |
| Persist the user's choice | preference | Restore the same intent on the next launch. |
Common Mistakes
Don't compare picker rows against theme.name.
// Wrong. theme.name is always concrete; the 'system' row never lights up.
options.map((option) => (
<Pressable style={{ opacity: theme.name === option ? 1 : 0.5 }} />
))Do compare them against preference.
// Right. preference can be 'system'.
options.map((option) => (
<Pressable style={{ opacity: preference === option ? 1 : 0.5 }} />
))Don't toggle dark to light against preference.
// Wrong. When the user is in system mode, preference === 'system'
// and this branch sends them to 'light' even if the OS is currently dark.
setTheme(preference === 'dark' ? 'light' : 'dark')Do toggle against theme.name.
// Right. theme.name tracks what's painted, even in system mode.
setTheme(theme.name === 'dark' ? 'light' : 'dark')Example
import { Pressable, Text, View } from 'react-native'
import { useTheme } from '@/lib/theme'
export function Card() {
const { theme, preference, setTheme, isTransitioning } = useTheme()
return (
<View style={{ backgroundColor: theme.colors.background, padding: 16 }}>
<Text style={{ color: theme.colors.text }}>
Painted: {theme.name}
</Text>
<Text style={{ color: theme.colors.textSecondary }}>
Preference: {preference}
</Text>
<Pressable
disabled={isTransitioning}
onPress={() => setTheme(theme.name === 'dark' ? 'light' : 'dark')}
>
<Text style={{ color: theme.colors.primary }}>Toggle</Text>
</Pressable>
</View>
)
}setTheme
setTheme(name, options?): 'accepted' | 'ignored'Changes the user's preference. The new preference is reflected
synchronously in subsequent reads of preference, so picker UIs
that highlight against it can repaint before the snapshot is
captured.
Arguments
| Name | Type | Description |
|---|---|---|
name | Names | 'system' | Concrete theme name, or 'system' to follow the OS appearance. |
options | SetThemeOptions | Optional per-call configuration. See SetThemeOptions for every variant and field. |
Returns
| Value | Meaning |
|---|---|
'accepted' | The library will apply the change, instantly or via an animated transition. |
'ignored' | A transition is already in flight, or the call targets the same preference that's already active. |
Most callers can discard the return value. isTransitioning and the
configured callbacks cover the common cases. The discriminated
string is there for telemetry callers who want to know explicitly
whether a call did anything.
Common Options
// Default fade.
setTheme('dark')
// Circular reveal from a button ref.
setTheme('dark', { transition: 'circularReveal', origin: buttonRef })
// Wipe right (new theme enters from the left edge).
setTheme('dark', { transition: 'wipe', direction: 'right', duration: 500 })
// Split closing inward like shutters.
setTheme('dark', { transition: 'split', mode: 'top-bottom', inverted: true })
// Pixelize with a chunkier mosaic at the peak.
setTheme('dark', { transition: 'pixelize', blockSize: 40 })
// Instant switch (no snapshot, no overlay).
setTheme('dark', { animated: false })isTransitioning
true from just after the snapshot is captured until the animation
completes. Use it to disable interactive controls so a second call
can't be queued mid-transition.
<Pressable disabled={isTransitioning} onPress={() => setTheme('dark')}>
<Text>Dark</Text>
</Pressable>Touch input is also blocked internally from the moment setTheme
is called, even before isTransitioning flips, since the snapshot
hasn't been captured yet. The flag is there for visual feedback
like disabled={isTransitioning}. The library protects you from
the race.
Picker Pattern
preference updates synchronously inside setTheme, so pickers can
highlight against it directly. No local picked state is needed.
The engine waits one frame before capturing the snapshot, which
gives React time to commit and paint the new highlight.
import { Pressable, Text, View } from 'react-native'
import { useTheme } from '@/lib/theme'
const OPTIONS = ['light', 'dark', 'system'] as const
export function ThemePicker() {
const { theme, preference, setTheme, isTransitioning } = useTheme()
return (
<View style={{ flexDirection: 'row', gap: 8 }}>
{OPTIONS.map((option) => (
<Pressable
key={option}
disabled={isTransitioning}
onPress={() => setTheme(option)}
style={{
backgroundColor:
preference === option ? theme.colors.primary : theme.colors.surface,
}}
>
<Text style={{ color: theme.colors.text }}>{option}</Text>
</Pressable>
))}
</View>
)
}See Theme Picker for the persisted-state variation.
Runtime Errors
setTheme validates its numeric options at every call and throws a
descriptive Error if any value is out of range. The check runs
before the same-theme and in-flight guards, so misuse fails fast at
the call site instead of producing a silently broken animation.
| Error | Cause |
|---|---|
`duration` must be a non-negative finite number | duration is negative, NaN, or Infinity. |
`blockSize` must be a finite number >= 2 | blockSize is below 2, NaN, or Infinity. |
`noiseSize` must be a finite number >= 1 | noiseSize is below 1, NaN, or Infinity. |
`origin` coordinates must be finite numbers | An explicit origin point has a NaN or Infinity coordinate. (Refs are validated at measurement time and fall back to the screen center if measurement fails.) |
These validations are intentionally strict because the affected
values feed shaders and geometry math where silent NaN propagation
produces empty or visually broken transitions instead of a useful
crash.
Behavior Rules
- Same preference. Returns
'ignored'. - Already transitioning. Returns
'ignored'. Wait forisTransitioningto becomefalsebefore retrying. 'system'. Enters system-following mode and subscribes to OS changes.preferencebecomes'system', whiletheme.nameholds the OS-resolved concrete theme.- Concrete name. Exits system-following mode.
animated: false. Instant switch. OnlyonThemeChangefires.- Snapshot capture failure. Falls back to an instant switch.
onThemeChangestill fires, butonTransitionEnddoes not.
See Also
- createThemeTransition. The factory that produces the hook.
- SetThemeOptions. Every variant and field.
- How transitions work. Capture, swap, animate.
- Callbacks. When each callback fires.