Platform Blocks Theming
Platform Blocks theming is a single PlatformBlocksTheme object distributed by
PlatformBlocksProvider through React context. Two complete themes ship with the
library (DEFAULT_THEME light, DARK_THEME dark) and the provider switches between
them automatically — unless you hand it a theme of your own, which turns that
switching off (see "Theme resolution rules").
All public APIs import from the package root:
import {
PlatformBlocksProvider, PlatformBlocksThemeProvider, ThemeModeProvider,
useTheme, useThemeVisuals, useThemeLayout, useThemeMode, useColorScheme,
createTheme, DEFAULT_THEME, DARK_THEME,
resolveVariantRoles, CORE_COLORS,
withAlpha, readableTextOn, contrastRatio, composite, pickReadable,
Surface, useSurfaceLevel,
type PlatformBlocksTheme, type PlatformBlocksThemeOverride,
type ThemeModeConfig, type ColorSchemeMode,
} from '@platform-blocks/ui';
Core workflow
- Mount the provider once at the app root. With no
theme prop it renders
DEFAULT_THEME in light and {...DEFAULT_THEME, ...DARK_THEME, colorScheme: 'dark'}
in dark, following the OS by default (colorSchemeMode="auto").
- Add user-controlled light/dark/auto by passing
themeModeConfig (see
ThemeModeConfig in api.md). That mounts ThemeModeProvider internally and makes
useThemeMode() available anywhere below: { mode, setMode, cycleMode, actualColorScheme }.
- Customize colors/tokens either with a partial override
(
theme={createTheme({...})} — merged over DEFAULT_THEME, light-only) or with two
complete brand themes you switch yourself (the only way to keep dark mode with a
custom theme — full recipe in patterns.md).
- Read tokens in custom components with
useTheme() (full theme), or the
granular useThemeVisuals() / useThemeLayout() slices to avoid unrelated re-renders.
- Express elevation with the
Surface component (level={0..3} or raised)
instead of hand-picking backgrounds; it consumes theme.surfaces.
- Color custom components with
resolveVariantRoles(theme, { variant, color })
so your fills/borders/text match Button/Chip/Badge on any theme.
Theme resolution rules (verify against these before advising)
The provider resolves the active theme in this exact order
(PlatformBlocksProvider.tsx):
theme prop present → used as-is for every color scheme. Built-in light/dark
switching is bypassed entirely. Downstream, PlatformBlocksThemeProvider then:
- object has a truthy
colorScheme → treated as a complete theme, used directly;
- otherwise → treated as a partial override and deep-merged over
DEFAULT_THEME
(so a partial override is always light + your changes, even in dark mode).
- no
theme prop → scheme comes from themeModeConfig (its resolved
actualColorScheme) if provided, else from the colorSchemeMode prop
('auto' | 'light' | 'dark', default 'auto' = OS preference); dark resolves to
the built-in dark theme, light to DEFAULT_THEME.
- When
themeModeConfig is passed, the colorSchemeMode prop is ignored.
Key concepts
Theme anatomy — primaryColor, colorScheme, colors (10-shade ramps:
primary, secondary, tertiary, surface, success, warning, error, gray,
highlight, plus optional pink/purple/violet/cyan/lime/sky/amber/indigo/teal),
text (primary/secondary/muted/disabled/link/onPrimary), backgrounds
(base/subtle/surface/elevated/border), optional surfaces (elevation ladder 0-3),
optional states (focusRing/textSelection/highlightText/highlightBackground),
fontFamily, fontSizes/spacing/radii (xs..3xl), shadows (xs..xl),
breakpoints, motion, semantic (accent/borderDefault/borderSubtle/
surfaceElevated/surfaceCard/focusRing), components, other, designTokens.
Full annotated shape in references/api.md.
Ramps invert between schemes. Light ramps run lightest [0] → darkest [9];
dark ramps run the reverse. Index [5] is always the vivid base, and a higher
index always means more contrast against the current surface in both schemes.
Never hardcode "low index = light".
Surfaces ladder — theme.surfaces[0..3] maps elevation to
{ background, border, shadow } as a set: 0 = page, 1 = resting cards, 2 = dropdowns/
popovers, 3 = dialogs/toasts. Light mode conveys elevation with shadow (fill stays
white); dark mode with a lighter fill + hairline border. Themes that omit surfaces
get a ladder derived from backgrounds. Use <Surface level={n}> or
<Surface raised> (parent level + 1, clamped at 3, read via useSurfaceLevel()).
Variant roles — resolveVariantRoles(theme, { variant, color }) returns
{ fill, border, text } for 'filled' | 'outline' | 'light' | 'subtle' | 'surface' | 'gradient'. color is one of CORE_COLORS
(primary/secondary/success/warning/error/gray) or any raw hex/CSS color. Text is
picked by measured WCAG contrast (pickReadable, readableTextOn), tints are alpha
washes over the live surface (withAlpha), so results stay legible on custom themes.
Web only — the provider injects --platform-blocks-* CSS variables
(withCSSVariables, default true, under cssVariablesSelector, default ':root')
and always stamps data-platform-blocks-color-scheme="light|dark" on <html>. With
themeModeConfig, a manual (non-auto) choice also adds class
platform-blocks-light / platform-blocks-dark and
data-platform-blocks-manual="light|dark" to <html> (configurable via
themeModeConfig.domConfig). Mode persists to localStorage key
platform-blocks-theme-mode by default. Variable list in api.md.
Pitfalls
- Passing any
theme prop kills built-in dark mode. To keep switching with
brand colors, build complete light + dark themes and select between them with
your own ThemeModeProvider wrapper (patterns.md "Custom palette with dark mode").
- A partial override never goes dark.
createTheme({ primaryColor: ... }) has no
colorScheme, so it merges over DEFAULT_THEME — permanently light.
useThemeMode() throws unless themeModeConfig was passed to
PlatformBlocksProvider (or you mounted ThemeModeProvider yourself).
persistence.get must be synchronous (() => ColorSchemeMode | null). The
default localStorage persistence is a no-op on native — use a sync store like
react-native-mmkv; AsyncStorage does not fit this API.
- Not root exports:
mergeTheme, BUILT_IN_DARK_THEME, resolveSurface,
surfaceInteractionTint, resolveGradientStops, CSSVariables are internal.
Compose custom themes with object spreads over DEFAULT_THEME/DARK_THEME, and
use the Surface component instead of resolveSurface.
DARK_THEME omits designTokens. When building a custom dark theme, spread
...DEFAULT_THEME first, then ...DARK_THEME (that is exactly how the built-in
dark theme is composed).
theme.spacing/radii/fontSizes are CSS px strings ('16px'), not numbers.
In React Native styles use the numeric helpers (getSpacing, getIconSize,
getHeight, resolveSize, SIZE_SCALES) or parseInt the token.
- Keep a custom
theme object referentially stable (module scope or useMemo) —
the provider assumes it is stable and re-renders the whole tree when it changes.
useTheme() never throws — outside a provider it silently returns
DEFAULT_THEME, which can mask a missing provider during debugging.
- Don't hand-pick palette indices for text on tinted fills — contrast flips
between schemes. Use
resolveVariantRoles, pickReadable, or readableTextOn.
References
- references/api.md — full
PlatformBlocksTheme shape, provider props and
resolution logic, ThemeModeConfig, all hooks, surfaces/variant/color utilities,
the complete CSS-variable list, and the root export map.
- references/patterns.md — complete copy-paste examples: provider setup, theme
switcher UI, persisted mode config (web + native), custom brand palettes with and
without dark mode, reading tokens, Surface elevation, variant-aware custom
components, nested sub-tree themes, and web CSS integration.
Anything this skill does not cover
This skill covers the theme object, color schemes, palettes, surfaces, and
variant color resolution. Platform Blocks is much larger — 97 components, 25
charts, and 18 hooks. Do not guess an API for something outside this scope;
fetch the generated docs instead:
| What you need |
Where |
| Index of every page, one line each |
https://platform-blocks.com/llms.txt |
| One component or chart |
https://platform-blocks.com/llms/components/<Name>.md |
| One hook |
https://platform-blocks.com/llms/hooks/<useName>.md |
| Guides |
https://platform-blocks.com/llms/guides/{getting-started,accessibility,localization}.md |
| Everything in one file (~1.3 MB) |
https://platform-blocks.com/llms-full.txt |
<Name> is the exact PascalCase export name — .../llms/components/DataTable.md,
.../llms/components/AreaChart.md. Each page carries the component's full prop
table (type, required, default, description) plus runnable examples, generated
from the source, so it is authoritative where memory is not. When you are unsure
whether something exists or what it is called, read llms.txt first — it lists
every page with a one-line summary.
Import paths: components come from the package root (import { X } from '@platform-blocks/ui'). The exceptions are subpath-only: FormLayout
(@platform-blocks/ui/FormLayout), AudioPlayer
(@platform-blocks/ui/AudioPlayer), and the whole Navigation module —
NavigationContainer, createStackNavigator, createDrawerNavigator,
Screen, useNavigation, useRoute (@platform-blocks/ui/Navigation). A few
utilities also live on subpaths (e.g. validationRules on
@platform-blocks/ui/Input). A docs page existing does not guarantee a root
export — HoverCard, for instance, is internal and has no page and no export.
Notably outside this skill:
- Per-component styling props. Which
variant/size/colorVariant values a
given component accepts is on that component's page — theming defines what the
values mean, not which ones exist.
- Install and provider wiring → the
platform-blocks-setup skill. Layout
→ platform-blocks-layout. Chart theming → platform-blocks-charts.
1---2name: platform-blocks-theming3description: Theme the @platform-blocks/ui React Native library. Use when customizing the Platform Blocks theme (brand colors, ramps, spacing, radii, shadows), implementing dark mode or light/dark/auto color-scheme switching, defining custom color palettes, working with surfaces/elevation, resolving variant colors (filled/outline/light/subtle/surface/gradient), reading theme tokens in custom components, or styling web output via the injected --platform-blocks-* CSS variables and html classes.4---56# Platform Blocks Theming78Platform Blocks theming is a single `PlatformBlocksTheme` object distributed by9`PlatformBlocksProvider` through React context. Two complete themes ship with the10library (`DEFAULT_THEME` light, `DARK_THEME` dark) and the provider switches between11them automatically — unless you hand it a `theme` of your own, which turns that12switching off (see "Theme resolution rules").1314All public APIs import from the package root:1516```tsx17import {18 PlatformBlocksProvider, PlatformBlocksThemeProvider, ThemeModeProvider,19 useTheme, useThemeVisuals, useThemeLayout, useThemeMode, useColorScheme,20 createTheme, DEFAULT_THEME, DARK_THEME,21 resolveVariantRoles, CORE_COLORS,22 withAlpha, readableTextOn, contrastRatio, composite, pickReadable,23 Surface, useSurfaceLevel,24 type PlatformBlocksTheme, type PlatformBlocksThemeOverride,25 type ThemeModeConfig, type ColorSchemeMode,26} from '@platform-blocks/ui';27```2829## Core workflow30311. **Mount the provider once** at the app root. With no `theme` prop it renders32 `DEFAULT_THEME` in light and `{...DEFAULT_THEME, ...DARK_THEME, colorScheme: 'dark'}`33 in dark, following the OS by default (`colorSchemeMode="auto"`).342. **Add user-controlled light/dark/auto** by passing `themeModeConfig` (see35 `ThemeModeConfig` in api.md). That mounts `ThemeModeProvider` internally and makes36 `useThemeMode()` available anywhere below: `{ mode, setMode, cycleMode, actualColorScheme }`.373. **Customize colors/tokens** either with a partial override38 (`theme={createTheme({...})}` — merged over `DEFAULT_THEME`, light-only) or with two39 complete brand themes you switch yourself (the only way to keep dark mode with a40 custom theme — full recipe in patterns.md).414. **Read tokens in custom components** with `useTheme()` (full theme), or the42 granular `useThemeVisuals()` / `useThemeLayout()` slices to avoid unrelated re-renders.435. **Express elevation** with the `Surface` component (`level={0..3}` or `raised`)44 instead of hand-picking backgrounds; it consumes `theme.surfaces`.456. **Color custom components** with `resolveVariantRoles(theme, { variant, color })`46 so your fills/borders/text match Button/Chip/Badge on any theme.4748## Theme resolution rules (verify against these before advising)4950The provider resolves the active theme in this exact order51(`PlatformBlocksProvider.tsx`):5253- `theme` prop present → **used as-is for every color scheme**. Built-in light/dark54 switching is bypassed entirely. Downstream, `PlatformBlocksThemeProvider` then:55 - object has a truthy `colorScheme` → treated as a **complete** theme, used directly;56 - otherwise → treated as a partial override and deep-merged over `DEFAULT_THEME`57 (so a partial override is *always light* + your changes, even in dark mode).58- no `theme` prop → scheme comes from `themeModeConfig` (its resolved59 `actualColorScheme`) if provided, else from the `colorSchemeMode` prop60 (`'auto' | 'light' | 'dark'`, default `'auto'` = OS preference); dark resolves to61 the built-in dark theme, light to `DEFAULT_THEME`.62- When `themeModeConfig` is passed, the `colorSchemeMode` prop is ignored.6364## Key concepts6566**Theme anatomy** — `primaryColor`, `colorScheme`, `colors` (10-shade ramps:67`primary`, `secondary`, `tertiary`, `surface`, `success`, `warning`, `error`, `gray`,68`highlight`, plus optional `pink/purple/violet/cyan/lime/sky/amber/indigo/teal`),69`text` (primary/secondary/muted/disabled/link/onPrimary), `backgrounds`70(base/subtle/surface/elevated/border), optional `surfaces` (elevation ladder 0-3),71optional `states` (focusRing/textSelection/highlightText/highlightBackground),72`fontFamily`, `fontSizes`/`spacing`/`radii` (xs..3xl), `shadows` (xs..xl),73`breakpoints`, `motion`, `semantic` (accent/borderDefault/borderSubtle/74surfaceElevated/surfaceCard/focusRing), `components`, `other`, `designTokens`.75Full annotated shape in references/api.md.7677**Ramps invert between schemes.** Light ramps run lightest `[0]` → darkest `[9]`;78dark ramps run the reverse. Index `[5]` is always the vivid base, and a *higher*79index always means *more contrast against the current surface* in both schemes.80Never hardcode "low index = light".8182**Surfaces ladder** — `theme.surfaces[0..3]` maps elevation to83`{ background, border, shadow }` as a set: 0 = page, 1 = resting cards, 2 = dropdowns/84popovers, 3 = dialogs/toasts. Light mode conveys elevation with shadow (fill stays85white); dark mode with a lighter fill + hairline border. Themes that omit `surfaces`86get a ladder derived from `backgrounds`. Use `<Surface level={n}>` or87`<Surface raised>` (parent level + 1, clamped at 3, read via `useSurfaceLevel()`).8889**Variant roles** — `resolveVariantRoles(theme, { variant, color })` returns90`{ fill, border, text }` for `'filled' | 'outline' | 'light' | 'subtle' | 'surface' |91'gradient'`. `color` is one of `CORE_COLORS`92(`primary/secondary/success/warning/error/gray`) or any raw hex/CSS color. Text is93picked by measured WCAG contrast (`pickReadable`, `readableTextOn`), tints are alpha94washes over the live surface (`withAlpha`), so results stay legible on custom themes.9596**Web only** — the provider injects `--platform-blocks-*` CSS variables97(`withCSSVariables`, default true, under `cssVariablesSelector`, default `':root'`)98and always stamps `data-platform-blocks-color-scheme="light|dark"` on `<html>`. With99`themeModeConfig`, a *manual* (non-auto) choice also adds class100`platform-blocks-light` / `platform-blocks-dark` and101`data-platform-blocks-manual="light|dark"` to `<html>` (configurable via102`themeModeConfig.domConfig`). Mode persists to localStorage key103`platform-blocks-theme-mode` by default. Variable list in api.md.104105## Pitfalls1061071. **Passing any `theme` prop kills built-in dark mode.** To keep switching with108 brand colors, build complete light + dark themes and select between them with109 your own `ThemeModeProvider` wrapper (patterns.md "Custom palette with dark mode").1102. **A partial override never goes dark.** `createTheme({ primaryColor: ... })` has no111 `colorScheme`, so it merges over `DEFAULT_THEME` — permanently light.1123. **`useThemeMode()` throws** unless `themeModeConfig` was passed to113 `PlatformBlocksProvider` (or you mounted `ThemeModeProvider` yourself).1144. **`persistence.get` must be synchronous** (`() => ColorSchemeMode | null`). The115 default localStorage persistence is a no-op on native — use a sync store like116 `react-native-mmkv`; `AsyncStorage` does not fit this API.1175. **Not root exports:** `mergeTheme`, `BUILT_IN_DARK_THEME`, `resolveSurface`,118 `surfaceInteractionTint`, `resolveGradientStops`, `CSSVariables` are internal.119 Compose custom themes with object spreads over `DEFAULT_THEME`/`DARK_THEME`, and120 use the `Surface` component instead of `resolveSurface`.1216. **`DARK_THEME` omits `designTokens`.** When building a custom dark theme, spread122 `...DEFAULT_THEME` first, then `...DARK_THEME` (that is exactly how the built-in123 dark theme is composed).1247. **`theme.spacing`/`radii`/`fontSizes` are CSS px strings** (`'16px'`), not numbers.125 In React Native styles use the numeric helpers (`getSpacing`, `getIconSize`,126 `getHeight`, `resolveSize`, `SIZE_SCALES`) or `parseInt` the token.1278. **Keep a custom `theme` object referentially stable** (module scope or `useMemo`) —128 the provider assumes it is stable and re-renders the whole tree when it changes.1299. **`useTheme()` never throws** — outside a provider it silently returns130 `DEFAULT_THEME`, which can mask a missing provider during debugging.13110. **Don't hand-pick palette indices for text on tinted fills** — contrast flips132 between schemes. Use `resolveVariantRoles`, `pickReadable`, or `readableTextOn`.133134## References135136- **references/api.md** — full `PlatformBlocksTheme` shape, provider props and137 resolution logic, `ThemeModeConfig`, all hooks, surfaces/variant/color utilities,138 the complete CSS-variable list, and the root export map.139- **references/patterns.md** — complete copy-paste examples: provider setup, theme140 switcher UI, persisted mode config (web + native), custom brand palettes with and141 without dark mode, reading tokens, Surface elevation, variant-aware custom142 components, nested sub-tree themes, and web CSS integration.143144## Anything this skill does not cover145146This skill covers the theme object, color schemes, palettes, surfaces, and147variant color resolution. Platform Blocks is much larger — 97 components, 25148charts, and 18 hooks. Do not guess an API for something outside this scope;149fetch the generated docs instead:150151| What you need | Where |152| --- | --- |153| Index of every page, one line each | `https://platform-blocks.com/llms.txt` |154| One component or chart | `https://platform-blocks.com/llms/components/<Name>.md` |155| One hook | `https://platform-blocks.com/llms/hooks/<useName>.md` |156| Guides | `https://platform-blocks.com/llms/guides/{getting-started,accessibility,localization}.md` |157| Everything in one file (~1.3 MB) | `https://platform-blocks.com/llms-full.txt` |158159`<Name>` is the exact PascalCase export name — `.../llms/components/DataTable.md`,160`.../llms/components/AreaChart.md`. Each page carries the component's full prop161table (type, required, default, description) plus runnable examples, generated162from the source, so it is authoritative where memory is not. When you are unsure163whether something exists or what it is called, read `llms.txt` first — it lists164every page with a one-line summary.165166Import paths: components come from the package root (`import { X } from167'@platform-blocks/ui'`). The exceptions are subpath-only: `FormLayout`168(`@platform-blocks/ui/FormLayout`), `AudioPlayer`169(`@platform-blocks/ui/AudioPlayer`), and the whole `Navigation` module —170`NavigationContainer`, `createStackNavigator`, `createDrawerNavigator`,171`Screen`, `useNavigation`, `useRoute` (`@platform-blocks/ui/Navigation`). A few172utilities also live on subpaths (e.g. `validationRules` on173`@platform-blocks/ui/Input`). A docs page existing does not guarantee a root174export — `HoverCard`, for instance, is internal and has no page and no export.175176Notably outside this skill:177178- **Per-component styling props.** Which `variant`/`size`/`colorVariant` values a179 given component accepts is on that component's page — theming defines what the180 values mean, not which ones exist.181- **Install and provider wiring** → the `platform-blocks-setup` skill. **Layout**182 → `platform-blocks-layout`. **Chart theming** → `platform-blocks-charts`.