What You'll Build
You will build a production-grade, multi-theme token system for a cricket statistics platform — a tiered token architecture (primitive, semantic, component) implemented as CSS variables mapped to Tailwind utilities, driving a small component set that supports light, dark, and at least two brand themes, all switchable at runtime with zero component changes. This is the capstone of the Production & Scale module, integrating tokens, theming, the config, plugins, accessibility, and the component patterns into one cohesive, scalable system.
Unlike the earlier component-library exercise, the focus here is the token architecture and its scalability: proving that a brand refresh is a token edit, a new white-label client is a theme block, and dark mode is just another theme — none of which touch component code. You will also harden it for production: enforce the semantic-only rule, optimise the build, and verify accessibility across every theme.
The deliverable demonstrates the senior-level capability this whole module builds toward: not styling a component, but architecting a system that keeps many products coherent and re-themeable at organisational scale. This is the work that distinguishes building a design system from using a CSS framework.
Prerequisites
- Design-token tiers from Lesson 29 — primitive, semantic, and component tokens, and the rule that components reference only semantic tokens.
- Variable-backed theming from Lesson 14 — CSS custom properties remapped per theme via a root class, with the <alpha-value> placeholder for slash-opacity support.
- Config customization from Lesson 15 — mapping semantic tokens to named utilities and using theme.extend correctly.
- Production optimisation from Lesson 25 — accurate content globs, literal classes, and measuring the gzipped build size.
- Accessibility from Lesson 27 — focus-visible rings, contrast verification in every theme, and not relying on color alone.
- Component patterns from Lesson 28 — the cn helper (clsx + tailwind-merge), cva variant APIs, and merging the caller className last.
Setup & Project Structure
Structure the project around the token tiers so the architecture is visible in the file layout: a primitives file, a semantics-and-themes file, the config mapping semantics to utilities, a small component layer speaking only semantics, and a showcase with a theme switcher. Install the helper tooling and a lint rule to forbid raw color values in components. Confirm the content globs cover everything and the dev server runs.
# Setup
npm create vite@latest cric-design-system -- --template react && cd cric-design-system
npm install && npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p
npm install clsx tailwind-merge class-variance-authority
# Structure — tiers visible in the layout
# src/
# ├─ tokens/
# │ ├─ primitives.css <- raw palette + scales (--blue-700, --space-4, ...)
# │ └─ themes.css <- semantic tokens per theme (:root, .dark, .theme-emerald, .theme-rose)
# ├─ lib/
# │ ├─ cn.js <- clsx + tailwind-merge
# │ └─ { Button, Card, Badge, Input, StatTile }.jsx <- semantic tokens ONLY
# └─ Showcase.jsx <- theme switcher + component gallery
# tailwind.config.js <- maps SEMANTIC tokens to utilities
# .eslintrc <- rule forbidding raw hex / arbitrary color values in components
npm run devStep 1 — Foundation
Build the token tiers first — this is the system's spine. Define primitive tokens (the raw ramps and scales), then semantic tokens that map roles onto primitives for the light theme, then map the semantic tokens to Tailwind utilities in the config. Prove the foundation with one component (Button) that references only semantic tokens. Getting the tiers and one component working validates the whole architecture before scaling.
/* src/tokens/primitives.css — PRIMITIVE tier */
:root {
--blue-700: 11 61 145; --blue-400: 96 165 250;
--emerald-600: 5 150 105; --emerald-400: 52 211 153;
--rose-600: 225 29 72; --rose-400: 251 113 133;
--slate-50: 248 250 252; --slate-900: 15 23 42; --slate-500: 100 116 139;
}
/* src/tokens/themes.css — SEMANTIC tier, one mapping per theme */
:root { /* light (default) */
--color-primary: var(--blue-700);
--color-surface: var(--slate-50);
--color-body: var(--slate-900);
--color-muted: var(--slate-500);
}
.dark {
--color-primary: var(--blue-400);
--color-surface: var(--slate-900);
--color-body: var(--slate-50);
}
// tailwind.config.js — expose SEMANTIC tokens as utilities
export default { darkMode: 'class', content: ['./index.html','./src/**/*.{js,jsx}'],
theme: { extend: { colors: {
primary: 'rgb(var(--color-primary) / <alpha-value>)',
surface: 'rgb(var(--color-surface) / <alpha-value>)',
body: 'rgb(var(--color-body) / <alpha-value>)',
muted: 'rgb(var(--color-muted) / <alpha-value>)'
}}}};Step 2 — Core Logic
Add the brand themes and the rest of the component set. Each brand is a new semantic-layer mapping (a .theme-emerald and .theme-rose block remapping the same semantic tokens onto different primitives), and each new component (Card, Badge, Input, StatTile) references only semantic tokens. This proves the core claim: adding a theme touches no components, and adding a component automatically works in every theme.
/* src/tokens/themes.css — BRAND themes: remap the SEMANTIC layer only */
.theme-emerald {
--color-primary: var(--emerald-600);
--color-surface: var(--slate-50);
--color-body: var(--slate-900);
}
.theme-emerald.dark { /* brand + dark compose */
--color-primary: var(--emerald-400);
--color-surface: var(--slate-900);
--color-body: var(--slate-50);
}
.theme-rose {
--color-primary: var(--rose-600);
--color-surface: var(--slate-50);
--color-body: var(--slate-900);
}
// src/lib/Card.jsx + Badge.jsx — semantic tokens ONLY, work in every theme
import { cn } from './cn';
export function Card({ className, ...p }) {
return <div {...p} className={cn('bg-surface text-body rounded-xl p-5 border border-muted/20', className)} />;
}
export function Badge({ className, ...p }) {
return <span {...p} className={cn('inline-flex px-2 py-0.5 rounded-full text-xs bg-primary/15 text-primary', className)} />;
}
// No dark: or theme-specific classes anywhere in components — themes rescope the tokens.Step 3 — Integration & Enhancement
Build the theme switcher and showcase, then harden for production. The switcher applies the theme class (and dark toggle) to the root element, persisting the choice and defaulting dark to system preference. The showcase displays the component set so you can watch the entire UI re-theme instantly as you switch. Add the lint rule forbidding raw color values in components, and run the production build to measure the optimised size.
// src/Showcase.jsx — runtime theme switcher proving zero-component-change theming
import { useState, useEffect } from 'react';
import { Button } from './lib/Button';
import { Card } from './lib/Card';
import { Badge } from './lib/Badge';
import { StatTile } from './lib/StatTile';
const BRANDS = ['', 'theme-emerald', 'theme-rose']; // '' = default blue
export function Showcase() {
const [brand, setBrand] = useState('');
const [dark, setDark] = useState(() => matchMedia('(prefers-color-scheme: dark)').matches);
useEffect(() => {
const root = document.documentElement;
root.className = [brand, dark ? 'dark' : ''].filter(Boolean).join(' '); // rescope tokens
}, [brand, dark]);
return (
<div className="min-h-screen bg-surface text-body p-8 space-y-6">
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-bold mr-auto">CricStat Design System</h1>
{BRANDS.map(b => (
<Button key={b} variant="secondary" size="sm" onClick={() => setBrand(b)}>
{b || 'Blue'}
</Button>
))}
<Button variant="secondary" size="sm" onClick={() => setDark(d => !d)}>
{dark ? 'Light' : 'Dark'}
</Button>
</div>
<Card className="max-w-md space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold">Rohit Sharma</h2>
<Badge>Captain</Badge>
</div>
<div className="grid grid-cols-2 gap-3">
<StatTile value="264" label="Highest ODI" />
<StatTile value="50" label="ODI tons" />
</div>
<Button>View full record</Button>
</Card>
{/* Switching brand or dark rescopes --color-* on the root: the WHOLE UI
re-themes instantly, and not one component was changed to support it. */}
</div>
);
}Step 4 — Testing & Verification
Run the app and cycle through every theme and the dark toggle, confirming the entire UI re-themes instantly with no flicker and no component-specific code involved. Verify contrast holds in every theme (especially dark and the brand combinations), tab through to confirm focus rings are visible against each theme's surface, and run the lint rule to confirm no component leaked a raw color. Finally, run the production build and measure the gzipped size.
# Verify
npm run dev
# Checklist:
# - Switching Blue / Emerald / Rose re-themes the ENTIRE UI instantly (no component edits)
# - Dark toggle composes with each brand (theme-emerald.dark, etc.) correctly
# - Contrast holds in every theme+mode combination (check body text on surface)
# - Focus-visible rings are clearly visible against each theme's surface (tab through)
# - No flicker on load (theme class set before first paint via an inline head script)
npm run lint # custom rule: FAILS if any component uses bg-[#...] or raw hex
npm run build && gzip -c dist/assets/*.css | wc -c # measure optimised, compressed size
# Expect a small CSS payload despite 4 themes: themes are variable values, not extra rules.Warning: Two production pitfalls to catch here. First, a flash of the wrong theme on load — set the theme and dark classes on the root via a tiny inline script in the document head before the bundle loads, not in a deferred effect. Second, a component that leaked a raw color (bg-[#...] or a primitive like bg-blue-700) will look fine in the default theme but fail to re-theme — the lint rule must catch these, since a single leak silently breaks the system's core promise.
Extension Challenge: (1) Generate the primitive and semantic tokens from a single source-of-truth tokens.json (or a Figma export) via a small build script, so design and code share one definition. (2) Add a high-contrast accessibility theme as another semantic mapping for low-vision users. (3) Package the token layer and components as a shared internal library (npm workspace) and consume it from a second demo app, proving the system scales across products, not just themes.
- A production token system is tiered — primitives, semantics, and components referencing only semantics — implemented as a CSS-variable chain that Tailwind maps to named utilities.
- Each theme (light, dark, and every brand) is a remapping of the semantic layer on a root class; adding a theme touches no component code, and adding a component automatically works in every theme.
- Components must speak only in semantic tokens with no dark: or theme-specific classes; themes work entirely by rescoping the variables the utilities resolve through.
- A runtime theme switcher applies the theme and dark classes to the root element, persisting the choice and defaulting dark to system preference, set before first paint to avoid flicker.
- Enforce the semantic-only rule with a lint rule that forbids raw hex and arbitrary color values in components, since one leak silently breaks re-theming.
- Verify contrast and visible focus in every theme-and-mode combination, not just the default, because a pairing that passes in light may fail in a dark or brand theme.
- The production CSS stays small despite many themes, because themes are variable values rescoped at runtime rather than additional generated rules — proven by measuring the gzipped build.