Skip to content
The Table Plan
← All posts

Design Tokens and Dark Mode

How the starter template ships a fully reskinnable UI from a single token file, and what that means for dark mode in practice.

One of the template's opinionated decisions is that every color in every component is a design token — never a raw hex value or a Tailwind color utility. This constraint sounds limiting, but it is what makes dark mode, white-label reskins, and future brand updates a one-file change.

The Token Contract

The token package lives at packages/webapp/design-tokens. It exposes two surfaces:

A "semantic" token is one named by its purpose, not its value: text-foreground instead of text-slate-900, bg-surface instead of bg-white. This is the key distinction — an element styled with text-foreground re-themes to dark mode automatically when the .dark class appears on <html>.

How Dark Mode Works

The template uses class-based dark mode (dark class on <html>) driven by next-themes. The single theme-toggle island in the marketing site header reads the system preference on first load and lets users override it.

/* In theme.css */
:root {
  --color-foreground: #0f172a;
  --color-background: #ffffff;
  --color-surface: #f8fafc;
}

.dark {
  --color-foreground: #f8fafc;
  --color-background: #0f172a;
  --color-surface: #1e293b;
}

When the .dark class is toggled, every CSS variable updates, and every component using those variables re-renders with the correct palette — no JavaScript re-render required for the Server Components (which are static HTML).

The Enforcement Gate

The validate:design-system lint gate runs in pnpm ci:validate and scans every .tsx, .ts, and .css file in the web tier for:

If any of these appear outside the explicitly allowlisted paths (the token package itself and each app's globals.css), the build fails. The gate has two escape hatches:

Intentional Exceptions

A design-system comment opts a single line out:

// design-system: intentional (PWA manifest spec requires literal hex; CSS vars don't apply)
themeColor: '#ffffff',

The comment must include a non-empty reason. The template ships only two such exceptions: the two PWA manifest theme colors.

Reskinning for a Fork

To change a fork's brand colors, open packages/webapp/design-tokens/src/tokens.ts, update the hex values under primary and any semantic overrides, and run pnpm build. Every component in both the marketing site and the product app picks up the new palette automatically. No component code changes required — that is the design-system gate earning its keep.

Token Classes vs Utility Classes

The distinction matters in practice:

| Use this | Not this | Why | | ---------------------------- | ---------------------- | ---------------------------------------------- | | bg-surface | bg-white | bg-white does not update in dark mode | | text-foreground | text-slate-900 | text-slate-900 is a fixed value | | border-neutral-200 | border-gray-200 | Use the token scale, not Tailwind's gray scale | | dark:border-neutral-800 | — | OK — token scale with dark variant |

When in doubt: if the class names a specific color rather than a role, it is probably not a token class.