Theming
Every color and radius the components read is a CSS custom property, defined once in
globals.css at :root (--background, --card, --primary, --chart-1…5,
--radius, and the rest). A page restyles itself by dropping a layout.tsx beside
its page: a default-exported Layout that wraps children in a
<div style={theme}> that reassigns those variables. The override is scoped to that
one route subtree, so nothing else on the site shifts.
This page ships exactly that file: a warm palette with squared corners, applied to everything below. View source to see the whole of it; in outline:
// capabilities/theming/layout.tsx
const theme = {
"--radius": "0.25rem", // was 0.625rem; cascades into --radius-sm/md/lg/xl
"--background": "#faf7f2",
"--primary": "#b45309", // amber, was near-black
"--chart-1": "#d97706", // warm ramp for every chart
// …the rest of the token set
} as CSSProperties;
export default function Layout({ children }) {
return (
<div style={theme} className="bg-background text-foreground">
{children}
</div>
);
}
Overridable tokens
There's no separate registry to learn: the catalog is globals.css :root. Every
variable defined there is a token you can override from a layout.tsx, and the
:root values are just the site defaults (each with a .dark counterpart in the
same file). The full set, by role:
- Surfaces:
--background/--foreground,--card/--card-foreground,--popover/--popover-foreground - Semantic roles:
--primary,--secondary,--muted,--accent(each with a-foregroundpair), plus--destructiveand--brand/--brand-foreground - Lines & focus:
--border,--input,--ring - Charts:
--chart-1through--chart-5, the qualitative ramp - Radius:
--radius, the single value from whichglobals.cssderives--radius-sm/md/lg/xl/2xl/3xl/4xl
One set is deliberately not page-overridable: the --sidebar-* tokens style the
site chrome, which lives outside any page's subtree, so reassigning them in a page
layout.tsx does nothing. Theme those in globals.css itself.
The same components, retinted
None of the components below know they've been themed. They read tokens by name,
so the palette above flows straight into them. Metrics and cards take
--card / --foreground, the chart takes the --chart-* ramp, the button takes
--primary, and every rounded corner takes --radius.
Scoping tighter than a page
layout.tsx is only a convenience: it themes the whole page because it wraps the
whole page. Underneath it's nothing but CSS custom properties on a style= object,
so you can set the same tokens on any element to re-theme just the subtree it wraps.
Overrides cascade, and tokens you don't set keep inheriting the theme above, so a
partial override rides on top of the page palette:
<div
style={{
"--primary": "#0f766e",
"--radius": "1rem",
"--chart-1": "#0891b2",
"--chart-2": "#0d9488",
"--chart-3": "#0e7490",
}}
>
<Card>{/* the same chart, now cool-toned and rounder */}</Card>
</div>
This card sits inside the page's warm theme, yet its chart marks, its corners, and
any --primary control read the cooler palette set one div up, while its --card
and --foreground, left unset, still inherit the warm page. Everything outside the
div stays warm. layout.tsx is just this same trick applied to the whole page.
Dark mode
An inline style= override sets each variable for both light and dark at once.
globals.css gives its tokens distinct .dark values, so a theme that must differ
by mode puts its dark overrides under a .dark selector rather than inline here.