Design System Starter Kit
A design system is not a component library. It's a set of constraints that make every design decision faster and every interface more consistent. Without one, every page looks like it was designed by a different person on a different day — because, in an AI-assisted workflow, it probably was. Claude Code generates beautiful components, but without constraints, each one invents its own spacing, colors, and typography.
This starter kit gives you the foundation: color tokens, typography, spacing, and component patterns. Fork it, customize it, and use it on every project.
Color Tokens
Colors are defined as CSS custom properties on :root (light mode) and [data-theme="dark"] (dark mode). Never use raw hex values in your components — always reference tokens. This way, switching themes means changing variables, not hunting through files.
/* /styles/tokens.css */
:root {
/* --- Brand --- */
--color-brand-primary: #2563eb; /* Blue 600 — primary actions, links */
--color-brand-primary-hover: #1d4ed8; /* Blue 700 — hover state */
--color-brand-secondary: #7c3aed; /* Violet 600 — accents, highlights */
/* --- Backgrounds --- */
--color-bg-primary: #ffffff; /* Main background */
--color-bg-secondary: #f9fafb; /* Cards, subtle sections */
--color-bg-tertiary: #f3f4f6; /* Input fields, code blocks */
--color-bg-inverse: #111827; /* Inverted sections (dark on light) */
/* --- Text --- */
--color-text-primary: #111827; /* Headings, body text */
--color-text-secondary: #4b5563; /* Descriptions, labels */
--color-text-tertiary: #9ca3af; /* Placeholders, disabled text */
--color-text-inverse: #f9fafb; /* Text on dark backgrounds */
--color-text-link: var(--color-brand-primary);
/* --- Borders --- */
--color-border-primary: #e5e7eb; /* Card borders, dividers */
--color-border-secondary: #d1d5db; /* Input borders */
--color-border-focus: var(--color-brand-primary);
/* --- Semantic (status colors) --- */
--color-success: #059669; /* Green 600 */
--color-success-bg: #ecfdf5; /* Green 50 */
--color-warning: #d97706; /* Amber 600 */
--color-warning-bg: #fffbeb; /* Amber 50 */
--color-error: #dc2626; /* Red 600 */
--color-error-bg: #fef2f2; /* Red 50 */
--color-info: #2563eb; /* Blue 600 */
--color-info-bg: #eff6ff; /* Blue 50 */
/* --- Shadows --- */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
[data-theme="dark"] {
--color-brand-primary: #60a5fa; /* Blue 400 — brighter on dark */
--color-brand-primary-hover: #93bbfd;
--color-brand-secondary: #a78bfa; /* Violet 400 */
--color-bg-primary: #0f172a; /* Slate 900 */
--color-bg-secondary: #1e293b; /* Slate 800 */
--color-bg-tertiary: #334155; /* Slate 700 */
--color-bg-inverse: #f8fafc;
--color-text-primary: #f1f5f9; /* Slate 100 */
--color-text-secondary: #94a3b8; /* Slate 400 */
--color-text-tertiary: #64748b; /* Slate 500 */
--color-text-inverse: #0f172a;
--color-text-link: var(--color-brand-primary);
--color-border-primary: #334155; /* Slate 700 */
--color-border-secondary: #475569; /* Slate 600 */
--color-border-focus: var(--color-brand-primary);
--color-success: #34d399; /* Emerald 400 */
--color-success-bg: #064e3b; /* Emerald 900 */
--color-warning: #fbbf24; /* Amber 400 */
--color-warning-bg: #78350f; /* Amber 900 */
--color-error: #f87171; /* Red 400 */
--color-error-bg: #7f1d1d; /* Red 900 */
--color-info: #60a5fa;
--color-info-bg: #1e3a5f;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4),
0 2px 4px -2px rgb(0 0 0 / 0.3);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5),
0 4px 6px -4px rgb(0 0 0 / 0.4);
}Design note: Dark mode is not inverted light mode. Notice that the brand color shifts from Blue 600 to Blue 400 — lighter values on dark backgrounds maintain the same perceived vibrancy. Semantic colors shift similarly: Red 600 becomes Red 400, Green 600 becomes Emerald 400. Shadows get heavier because dark backgrounds absorb more light.
Typography Scale
A modular scale based on a 1.25 ratio (major third). Each step is 1.25x the previous size. This creates visual harmony without arbitrary decisions.
:root {
/* --- Font Families --- */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont,
'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
/* --- Font Sizes --- */
--text-xs: 0.75rem; /* 12px — captions, badges */
--text-sm: 0.875rem; /* 14px — secondary text, labels */
--text-base: 1rem; /* 16px — body text (never go smaller) */
--text-lg: 1.125rem; /* 18px — lead paragraphs */
--text-xl: 1.25rem; /* 20px — card titles */
--text-2xl: 1.5rem; /* 24px — section headings */
--text-3xl: 1.875rem; /* 30px — page headings */
--text-4xl: 2.25rem; /* 36px — hero headings */
--text-5xl: 3rem; /* 48px — display headings */
/* --- Line Heights --- */
--leading-tight: 1.25; /* Headings (tight for large text) */
--leading-normal: 1.5; /* Body text (comfortable reading) */
--leading-relaxed: 1.75; /* Long-form content */
/* --- Font Weights --- */
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* --- Letter Spacing --- */
--tracking-tight: -0.025em; /* Large headings */
--tracking-normal: 0; /* Body text */
--tracking-wide: 0.025em; /* Uppercase labels, small caps */
}Heading hierarchy applied:
h1 {
font-size: var(--text-4xl);
font-weight: var(--weight-bold);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
}
h2 {
font-size: var(--text-3xl);
font-weight: var(--weight-semibold);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
}
h3 {
font-size: var(--text-2xl);
font-weight: var(--weight-semibold);
line-height: var(--leading-tight);
}
h4 {
font-size: var(--text-xl);
font-weight: var(--weight-medium);
line-height: var(--leading-normal);
}
p {
font-size: var(--text-base);
font-weight: var(--weight-normal);
line-height: var(--leading-normal);
}The rule of three weights: Use at most three font weights on any page. Typically: normal for body, medium for labels, bold or semibold for headings. More than three weights creates visual noise.
Spacing System
An 8px base grid. Every spacing value is a multiple of 4 or 8. This creates visual rhythm without thinking — elements snap to a grid the eye can feel even if it can't articulate.
:root {
--space-0: 0;
--space-1: 0.25rem; /* 4px — tight gaps, icon padding */
--space-2: 0.5rem; /* 8px — inline element spacing */
--space-3: 0.75rem; /* 12px — compact list items */
--space-4: 1rem; /* 16px — standard element gap */
--space-5: 1.25rem; /* 20px — form field spacing */
--space-6: 1.5rem; /* 24px — card internal padding */
--space-8: 2rem; /* 32px — section gap */
--space-10: 2.5rem; /* 40px — large section gap */
--space-12: 3rem; /* 48px — page section separation */
--space-16: 4rem; /* 64px — major section breaks */
--space-20: 5rem; /* 80px — hero-level spacing */
--space-24: 6rem; /* 96px — page-level breathing room */
/* --- Container Widths --- */
--container-sm: 640px; /* Narrow content (blog posts, forms) */
--container-md: 768px; /* Medium content */
--container-lg: 1024px; /* Standard page width */
--container-xl: 1280px; /* Wide layouts (dashboards, tables) */
/* --- Border Radius --- */
--radius-sm: 0.25rem; /* 4px — buttons, badges */
--radius-md: 0.5rem; /* 8px — cards, inputs */
--radius-lg: 0.75rem; /* 12px — modals, large cards */
--radius-xl: 1rem; /* 16px — hero sections */
--radius-full: 9999px; /* Circles, pill buttons */
}Spacing rules of thumb:
--space-4(16px) between sibling elements inside a component--space-6(24px) for container/card internal padding--space-8to--space-12between sections on a page--space-16or more for hero sections and major visual breaks- When it feels cramped, go up one step. When it feels wasteful, go down one step. Never invent values outside the scale.
Component Patterns
These are not full implementations — they're patterns showing how tokens compose into components. Adapt the selectors to your framework (Tailwind utility classes, CSS modules, styled-components).
Button
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
font-size: var(--text-sm);
font-weight: var(--weight-medium);
line-height: var(--leading-normal);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 150ms ease;
min-height: 44px; /* Touch target — non-negotiable */
min-width: 44px;
}
.btn-primary {
background: var(--color-brand-primary);
color: white;
border: none;
}
.btn-primary:hover { background: var(--color-brand-primary-hover); }
.btn-secondary {
background: transparent;
color: var(--color-text-primary);
border: 1px solid var(--color-border-secondary);
}
.btn-secondary:hover { background: var(--color-bg-tertiary); }
.btn-ghost {
background: transparent;
color: var(--color-text-secondary);
border: none;
}
.btn-ghost:hover {
color: var(--color-text-primary);
background: var(--color-bg-tertiary);
}Card
.card {
background: var(--color-bg-primary);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
padding: var(--space-6);
box-shadow: var(--shadow-sm);
}
.card-title {
font-size: var(--text-xl);
font-weight: var(--weight-semibold);
color: var(--color-text-primary);
}
.card-description {
font-size: var(--text-sm);
color: var(--color-text-secondary);
margin-top: var(--space-1);
}Input
.input {
width: 100%;
padding: var(--space-2) var(--space-3);
font-size: var(--text-base);
color: var(--color-text-primary);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-md);
min-height: 44px;
transition: border-color 150ms ease, box-shadow 150ms ease;
}
.input:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
}
.input::placeholder { color: var(--color-text-tertiary); }
.input-error { border-color: var(--color-error); }
.input-error:focus { box-shadow: 0 0 0 3px rgb(220 38 38 / 0.1); }Alert / Toast
.alert {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
}
.alert-success { background: var(--color-success-bg); color: var(--color-success); }
.alert-warning { background: var(--color-warning-bg); color: var(--color-warning); }
.alert-error { background: var(--color-error-bg); color: var(--color-error); }
.alert-info { background: var(--color-info-bg); color: var(--color-info); }Using This System
- Copy
tokens.cssinto your project's/styles/directory. Import it in your root layout. - Customize the brand colors. Change
--color-brand-primaryand its hover state. The structural tokens (background, text, border) work with any brand palette. - Respect the scale. Using values outside the system (random padding of 13px, font size of 17px) defeats the purpose. Constrain yourself to the tokens. Constraint creates speed.
- Extend, don't override. Need a new token? Add it to the system with a comment explaining when to use it. Don't create one-off values in components.
- Audit periodically. Run a search for raw color values (
#,rgb(,hsl() in your components. Every hit is a token that should exist but doesn't.
The goal is speed through constraint. When every color, size, and spacing value is pre-decided, you stop designing pixels and start designing experiences.