Theming & tokens
Reskin the whole site by editing CSS variables in global.css — the design tokens, the tri-state dark mode, and how components resolve them.
Every color, radius, shadow, and font in an astro-ignite site resolves through a
small set of CSS variables declared in src/styles/global.css. Components
never hardcode a color — they read tokens like --color-bg and --color-primary.
So a full reskin is a handful of edits in one file, not a sweep across the tree.
The token layer
Tokens live in a Tailwind v4 @theme block at the top of global.css. There are
two tiers:
- The zinc scale (
--color-zinc-50…--color-zinc-950) — raw palette values, theme-invariant. The single source of truth for every neutral. - Functional tokens — semantic names that point at the scale. Components only ever reference these:
| Token | Role |
|---|---|
--color-bg | Page background |
--color-surface, --color-surface-2 | Raised panels, insets |
--color-fg | Body text |
--color-fg-muted, --color-fg-subtle | Secondary / tertiary text |
--color-border, --color-border-strong | Hairlines, stronger dividers |
--color-primary, --color-primary-fg | Interactive fill + its text |
--color-ring | Focus ring |
--color-success / --color-warning / --color-danger | Functional status only |
Beyond color, the same block defines --font-display / --font-mono,
--radius(-sm/-md/-lg), --shadow(-sm), container widths, and the
--ease-out-soft motion curve.
How components read tokens
Components express color through Tailwind v4 utilities that resolve a token —
either the theme-mapped shorthand (bg-primary, text-fg, border-border) or
the arbitrary form (bg-[var(--color-bg)]). For example, the button atom’s
default variant is bg-primary text-primary-fg. Change --color-primary once
and every button, link, and focus state moves with it.
There is no separate stylesheet to keep in sync: inlineStylesheets: 'always'
ships the full stylesheet in the HTML on first paint, so a token edit is the
whole change.
Tri-state dark mode
The design is dark-first. With no class on <html>, the @theme values
apply. A .light class flips the functional tokens to their light values:
.light {
--color-bg: var(--color-zinc-50);
--color-fg: var(--color-zinc-950);
--color-primary: var(--color-zinc-900);
/* …the rest of the functional tokens, re-pointed at the scale */
}
The third state is “force dark on a light page” — a .dark class wins over
.light, wired through one declaration:
@variant dark (&:where(:not(.light), :not(.light) *));
ThemeToggle.astro flips the .light class and persists the choice to
localStorage; an anti-flash inline script in BaseLayout applies the stored
preference before first paint, so there’s no flicker. The first-visit default
differs by template: the docs template exposes it as
siteConfig.defaultTheme (light | dark | system, shipping light),
while the starter has no config knob — its anti-flash script falls back to
the system preference when nothing is stored.
Walkthrough: a real reskin
Give the site a violet brand accent and slightly rounder corners — without touching a single component.
- Add your brand color to the scale
Declare the new palette values alongside the zinc scale in the
@themeblock, so both themes can point at them:@theme { /* …existing zinc scale… */ --color-brand: oklch(62% 0.21 280); --color-brand-fg: var(--color-zinc-50); } - Re-point the functional tokens
Make
--color-primaryyour brand color (dark mode), and the focus ring a tint of it:@theme { /* functional tokens */ --color-primary: var(--color-brand); --color-primary-fg: var(--color-brand-fg); --color-ring: var(--color-brand); }Do the same inside
.lightso the accent holds in light mode. - Round the corners
The whole UI scales off four radius tokens. Bump them once:
@theme { --radius: 10px; --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; } - Run it
pnpm devand click around. Buttons, links, focus rings, badges, and cards all carry the new accent and radius — because they were reading tokens the whole time. No component edits, no find-and-replace.
Rules that keep it clean
- Tokens only in components — never
bg-zinc-900or a raw hex. The zinc scale exists solely as the source of token values. - Add a token before a one-off color. If two components need the same new color, it’s a token.
- Keep the
.lightblock in lock-step with@theme: every functional token you add needs a light value.
This is also the surface a planned customize-theme skill will drive — point
an agent at “make the site violet” and it edits these same tokens.