Skip to content

shutteringv1.0.0

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, --border as a [data-ground] block, or on :root / .dark for the default.
  • Plumbing — the reset, scrollbar, focus ring, and reduced-motion damping come from @shuttering/tokens/base.css (imported by theme.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 via cn so your classes win.
  • Components expose data-slot and 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.

Designed & built by stxkxs — React 19, Tailwind v4, and its own @shuttering/* packages.

Agent surfaces are always-on. Proprietary.

© stxkxs