100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Tailwind CSS & Modern CSS
65 minbeginner

Production Practice — Themeable Design System

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.

Analogy🏏Cricket
🏏 Think of it like cricket: This exercise is like a fielding-and-celebration drill session — rehearsing the dive-and-throw, the run-out relay, the choreographed wicket celebration — until each is smooth, repeatable, and perfectly timed. Just as those drills turn raw athleticism into crisp, reliable match-day moments, this exercise turns raw animation utilities into crisp, reliable interface moments. Just as a celebration that is mistimed or overdone looks worse than none, an effect that is janky or gratuitous looks worse than none. The insight is that interactive polish, like fielding flair, is drilled into something smooth and purposeful — practiced motion that lands cleanly every time.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: Set-piece effects are best choreographed in the playbook before you use them, just as a team drills its celebrations and fielding routines in advance. Just as the moves are defined and named in the playbook first so every player can call on them cleanly, you define your custom keyframes, slide-in, slide-out, fade, in the config up front, since several effects depend on them. Just as good coaching offers a calmer version of a routine for players who need it, you confirm motion-safe and motion-reduce work so users who prefer less motion are respected. Just as each drill is rehearsed on its own before being strung into a match routine, you keep each effect a small component so you can test it in isolation, then assemble the demo. The payoff: a clean foundation where every effect is pre-defined, considerate of motion preferences, and testable on its own before it goes live.
bash
# 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 dev

Step 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 grooves the fundamental fielding move — the clean pick-up and lift — the single action everything fancier builds on. Just as the basic clean pick-up must be flawless before attempting the diving relay, the basic hover-lift transition must be smooth before layering on more. Just as a fielder drills the simple action until it is second nature, you drill the transform-plus-transition pattern until it is reflexive. The insight is that the foundational motion must be clean first, because every richer effect is composed from it.
css
/* 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 2 is the warm-up-to-play transition — the team moving from net practice into the actual innings, a deliberate handover from preparation to performance. Just as the warm-up gives way smoothly to live play, the skeleton gives way smoothly to real content. Just as a jarring switch from nets straight to facing the new ball would unsettle a batsman, a jarring pop from skeleton to content unsettles the user. The insight is that the transition between a holding state and the real state should be as considered as the states themselves — the warm-up handover and the skeleton-to-content swap both reward a smooth changeover.
javascript
/* 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 3 is assembling the rehearsed pieces into the full match-day routine — the entrance, the play, the celebration, the walk-off — each transition choreographed to flow into the next. Just as the day's moments are stitched into one seamless production, your effects are stitched into one coherent interface. Just as the walk-off must be timed so players do not leave before the moment completes, the toast must stay mounted until its exit animation finishes. The insight is that integration is about timing the handovers between pieces — the match-day flow and the assembled UI both depend on each transition completing before the next begins.
javascript
// 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.

Analogy🏏Cricket
🏏 Think of it like cricket: You prove each effect by running it in isolation and then under real conditions, exactly like rehearsing a drill and then testing it match-day. Just as you check each set-piece plays out smoothly, you verify the stat card lifts and reveals its detail on hover, the loading skeleton swaps cleanly into content, the command bar shows its frosted-glass effect, and the toast fully completes its slide-out before disappearing. Just as you offer a calmer routine for players who need one, you enable reduced-motion emulation and confirm animations are disabled or calmed. Just as a shot must hold up on a slow, tiring pitch and not just a fast true one, you throttle the CPU to confirm the effects stay smooth on weaker hardware. The payoff: verified proof that every effect looks right, respects motion preferences, and performs even under poor conditions, not just on your fast machine.
bash
# 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.
Lesson 30 of 35
0% complete