Customizing per app
shuttering ships the defaults; an application expresses its identity as a short list of token overrides. Because every value resolves through a CSS custom property at runtime, overriding one re-themes the whole system — components included — with no component edits.
The override seam
An app owns a thin entry stylesheet that imports the theme, then redefines only the tokens it wants. Two rules keep the cascade predictable.
- Overrides load after
@shuttering/tokens/theme.css. - Prefer a later
@theme { … }block (or a:root { … }rule) so your value wins.
Fonts — bring your own
theme.css never hardcodes a typeface. Each font slot resolves to a --font-*-face var with a system fallback, so theme.css alone renders in system fonts; fill the three face vars to opt into your own. The font-* utilities, every component, and the canvas wordmarks all follow.
/* the app's entry stylesheet */
@import '@shuttering/tokens/theme.css'; /* tokens + Tailwind (system fonts) */
@import '@fontsource-variable/inter'; /* load your own faces */
@import '@fontsource/ibm-plex-mono';
:root { /* fill the bring-your-own seam */
--font-sans-face: 'Inter Variable', system-ui, sans-serif;
--font-mono-face: 'IBM Plex Mono', ui-monospace, monospace;
/* --font-display-face: your display face, or leave it for the serif fallback */
}
@source '../node_modules/@shuttering/ui/dist'; /* generate the components' classes */Want the shuttering trio (Fraunces / Schibsted Grotesk / Spline Sans Mono) instead? Add @import '@shuttering/tokens/fonts.css' — it self-hosts the faces and fills the three --font-*-face vars for you.
A custom ground
The ground is the surface set — background, card, popover, muted, border. Three ship (paper/void/slate); add your own the same way, as a [data-ground] block for light and dark.
@import '@shuttering/tokens/theme.css';
[data-ground='acme'] { /* light surfaces */
--background: #fbfbf8;
--foreground: #1a1a17;
--card: #ffffff;
--muted: #f2f2ec;
--border: #e6e6dd;
/* …the rest of the ground contract — see CONTRACT.md */
}
.dark[data-ground='acme'] { /* dark surfaces */
--background: #12120f;
--foreground: #ededea;
--card: #1a1a16;
--muted: #22221c;
--border: rgba(255, 255, 255, 0.08);
}Select it with data-ground="acme" (or drive it from useGround('acme')). Ground and data-palette are independent — any ground composes with any accent.
A custom brand palette
The six built-in palettes are just [data-palette] blocks supplying the accent tokens. Add a seventh the same way — light and dark — and switch to it.
@import '@shuttering/tokens/theme.css';
/* your brand accent, on top of the shared neutral ground */
[data-palette='acme'] {
--primary: #4f46e5; /* the accent */
--primary-foreground: #ffffff; /* text/icons on the accent */
--ring: #4f46e5; /* focus ring */
--brand-gradient: linear-gradient(135deg, #4f46e5, #9333ea);
}
.dark[data-palette='acme'] {
--primary: #818cf8;
--primary-foreground: #101413;
--ring: #818cf8;
--brand-gradient: linear-gradient(135deg, #818cf8, #c084fc);
}Then select it on the document element (or drive it from usePalette('acme')):
<html data-palette="acme">Only primary / primary-foreground / ring and the gradient change per palette — the neutral surfaces (background, card, muted, border) are shared, so a new brand stays coherent by construction.
Everything else is a token too
The same override pattern applies to the rest of the system.
- Ground / surfaces —
--background,--card,--muted,--foreground,--borderas a[data-ground]block, or on:root/.darkfor the default. - Plumbing — the reset, scrollbar, focus ring, and reduced-motion damping come from
@shuttering/tokens/base.css(imported bytheme.css); import it alone for the plumbing without an opinion. - Radius —
--radius-sm/md/lg. - Type scale — the
--text-*sizes and line heights. - Motion — the easing and duration variables (and respect the reduced-motion backstop).
Component-level tweaks
For the occasional bespoke instance — not a system-wide change — reach lower down the ladder.
- Every component takes
className, merged viacnso your classes win. - Components expose
data-slotand cva variants for targeted styling. - If a component needs different behavior, wrap it in the app rather than copying it — you keep getting upstream fixes on a version bump.
In short
Do: Express an app's identity as token overrides in one entry stylesheet — fonts, a [data-palette] block, radius. It cascades everywhere and survives upgrades.
Don’t: Fork a component to restyle it, or hardcode a hex in app markup — both drift from the system and lose upstream fixes.