Theming
Tokens describe the values. Theming is how a particular tree becomes the active one — and how a subtree opts into a different one without a render storm.
A theme is data
A theme is a plain object: a name plus a token tree. Two themes — light and dark — are
typically the whole story.
Both themes share the same primitive layer (ink, paper). Only the semantic layer (surface,
text) differs. Components reference the semantic paths and the right values arrive.
ThemeProvider makes themes available
<ThemeProvider> does two things at the top of the tree:
- Emits one
<style>element containing the CSS-variable declarations for every theme it knows about, scoped per[data-theme="<name>"]. - Wraps its children in a
<div data-theme={active}>.
The themes prop is the catalogue: every theme that might be active. The active prop is the
current one. Pass both — the catalogue stays stable, and active is the only thing that changes
when the user toggles.
Switches are attribute swaps
Because every theme's CSS variables are already on the page, a theme switch is one of:
- Web. Set
data-theme="dark"on the wrapper. The cascade picks up the dark block. No React re-render, no remount, no recomputation of styles. - Native. Update the
activeprop. The provider re-reads from the in-memory token tree and components consuminguseTheme()re-render.
The web path is the cheap one. Setting an attribute is a single DOM write; the browser handles
everything downstream. That is why a typical theme toggle reads from document.documentElement,
not from React state — the source of truth lives on the element, and the toggle just flips it.
Nested themes compose
A subtree can ask for a different theme without affecting the rest of the page. <Theme> sets a
new data-theme on its own wrapper.
Themes also chain. Inside a dark provider, <Theme name="red"> composes the names into a
dark_red lookup. If dark_red is in the themes array, that theme wins; otherwise motif falls
back to the inner name (red), and then the parent's active name. Pre-register the combinations
you care about and they activate automatically.
Same model, two renderers
<ThemeProvider> is a thin shell over the same tree on both platforms. On web, the tree becomes
CSS variables and the data-theme attribute is the activation switch. On native, the tree lives
in context and the runtime resolver reads from it directly.
The component code does not branch. <Box bg="$colors.surface.base" /> is the same on either
side; the platform decides how $colors.surface.base becomes a paint.