Skip to content
Expo Development Foundation
Esc
navigateopen⌘Jpreview
On this page

Theme tokens

Semantic palettes per appearance, explicit resolution, one theme hook.

Every app converges on the same shape: semantic color tokens defined per appearance, a pure resolution function mapping in-app preference + system scheme to an effective scheme, and exactly one hook delivering the active palette. Brand values differ per app; the shape does not.

When to use

  • Any app rendering in both light and dark appearances, or planning to.
  • Screens using translucent materials (expo-glass-effect), where text needs dedicated glass tokens with contrast targets — solid-surface tokens wash out on glass.

When not to use

  • A single-appearance prototype with no dark-mode plan — flat tokens (like the showcase’s own src/theme/colors.ts) are enough until the second appearance arrives.
  • Brand exploration itself — token values are a design task; this pattern covers only the structure that holds them.

File map

src/theme/palettes.ts      # AppTheme type + lightTheme/darkTheme (or colors/darkColors)
src/theme/appearance.ts    # pure resolveAppearance(mode, system) — no RN imports
src/theme/glass.ts         # glass-only tokens or capability flags, separate file
src/theme/<scales>.ts      # spacing, radius, typography, shadows (only what you use)
src/state/*-provider.tsx   # ThemeProvider + useAppTheme() (or useTheme())

Key excerpts (adapted)

One semantic type, two palettes — names are the contract, hexes are brand:

export type AppTheme = {
  background: string;
  card: string;
  border: string;
  text: string;
  secondaryText: string;
  accent: string;
  accentSoft: string;
  onAccent: string;
  success: string;
  danger: string;
};

Resolution is pure and explicit wins — “auto” follows the OS:

export function resolveAppearance(
  mode: 'auto' | 'light' | 'dark' | undefined,
  system: 'light' | 'dark' | 'unspecified' | null | undefined,
): 'light' | 'dark' {
  if (mode === 'light') return 'light';
  if (mode === 'dark') return 'dark';
  return system === 'dark' ? 'dark' : 'light';
}

One hook delivers the active palette; components never branch on appearance:

const { isDark, colors } = useAppTheme();
<Text style={[styles.title, { color: colors.text }]}>Title</Text>

Glass gets its own tokens, never the solid-surface set:

export const glassText = {
  light: { title: '#111815', body: '#202C26', subtext: '#2D3B34', /* … */ },
  dark: { title: '#F4F7F5', body: 'rgba(244, 247, 245, 0.94)', subtext: '#D2E0D8', /* … */ },
} as const;

Live demo

Open the (patterns)/theme-tokens route in the showcase/ app: an appearance switch (system / light / dark), the effective-scheme resolution, and a swatch grid of a neutral semantic palette in both appearances. Expo Go is enough.

Anti-patterns

  • A static exported theme object (“kept for non-UI logic”) that freezes one appearance — always resolve through the hook so dark mode adapts.
  • Hardcoding colors in StyleSheet.create — static styles can’t see the theme; inject dynamic colors at render time.
  • Reusing solid-surface secondary text on glass — frosted luminance washes it out; use the glass subtext token.
  • PlatformColor / Color.ios.* in themed views — they track the OS, not the in-app preference, and desync the two.
  • Omitting colorScheme on glass surfaces — it defaults to the OS appearance instead of the user’s in-app theme.
  • Splitting tokens across nine scale files before the app needs them — start with palettes + appearance, add spacing/typography scales on demand.

Provenance

Application labels are anonymized because the production source checkouts are private. They document observed SDK versions and file shapes, not publicly reproducible sources.

  • Production Expo 57 app B src/theme/*: AppTheme + palettes, pure resolveAppearance in appearance.ts, capability flags in glass.ts, appearance-provider.tsx delivery (including the deprecated-static-export mistake this pattern now forbids).
  • Production Expo 57 app C src/theme/colors.ts + DESIGN.md §2–§5: dual palettes, getThemeColors/getGlassColors, glassText contrast table, Rules 1–4.
  • Production Expo 53 app A src/theme/*: full scale system (spacing, radius, typography, shadows, zIndex) behind a single Theme interface and useTheme() hook.

Was this page helpful?