When I started my portfolio, I used AI-generated styles and borrowed component libraries. The result looked generic — indistinguishable from thousands of template sites. So I deleted everything and started from first principles.
This article documents the process of building a design system from scratch: the decisions behind every token, the mistakes I made, and the patterns that actually work.
Why Design Tokens
Design tokens are the atomic values of your visual language: colors, spacing, radii, shadows, typography. They're not just CSS variables — they encode design decisions. When you name a token --text-secondary, you're deciding that secondary text has a specific role and specific contrast ratio.
A design token is a named value that represents a design decision. It separates what something looks like from what it means.
- Consistency — every component pulls from the same source of truth
- Maintainability — change one token, update the entire app
- Theme support — swap token sets for dark mode, high contrast, or brand variants
- Communication — designers and developers share the same vocabulary
Color System
I use a layered color system with three levels: background hierarchy, text hierarchy, and semantic colors. Each level has exactly the stops it needs — no more.
:root {
/* ── background hierarchy ── */
--bg-base: #111318;
--bg-surface: #1a1d24;
--bg-elevated: #21252e;
/* ── text hierarchy ── */
--text-primary: #e4e6ea;
--text-secondary: #a0a5ae;
--text-tertiary: #6b7280;
--text-disabled: #454952;
/* ── semantic ── */
--color-primary: #4a8f85;
--color-success: #3d9970;
--color-warning: #c79a3a;
--color-danger: #c0504d;
}The muted teal primary (#4a8f85) was chosen deliberately. It's calm, professional, and passes WCAG AA contrast on dark backgrounds. Vibrant blues and purples are overused in developer portfolios — I wanted something quieter.
Typography Scale
I use a fixed scale instead of a mathematical ratio. Each step serves a specific purpose in the UI hierarchy. The base is 15px — slightly larger than the 14px default, which improves readability on screens.
.text-xs { font-size: 11px; line-height: 1.5; }
.text-sm { font-size: 13px; line-height: 1.5; }
.text-base { font-size: 15px; line-height: 1.6; }
.text-md { font-size: 17px; line-height: 1.5; }
.text-lg { font-size: 21px; line-height: 1.3; }
.text-xl { font-size: 26px; line-height: 1.2; }
.text-2xl { font-size: 32px; line-height: 1.15; }Line-height should decrease as font size increases. Headlines at 1.6 line-height look disconnected. Body text at 1.2 is unreadable. Match the two inversely.
Spacing & Grid
All spacing derives from an 8px base grid. This creates visual rhythm and makes alignment decisions automatic. The scale uses practical multiples: 4, 8, 12, 16, 24, 32, 48, 64, 96.
Component Patterns
Every component in the system follows three rules: it uses only design tokens (never hardcoded values), it accepts a className prop for composition, and it handles all its own states (hover, focus, disabled).
export function Badge({ children, variant = "default" }) {
const styles = {
default: "bg-[var(--color-primary-subtle)] text-[var(--color-primary)]",
outline: "border border-[var(--border-default)] text-[var(--text-tertiary)]",
muted: "bg-[var(--bg-elevated)] text-[var(--text-tertiary)]",
};
return (
<span className={`inline-flex items-center px-2 py-0.5
rounded-[var(--radius-badge)] text-[11px] font-semibold
${styles[variant]}`}>
{children}
</span>
);
}Dark Mode Done Right
Dark mode isn't just inverting colors. Each token needs a considered light-mode counterpart. The key insight: in dark mode, hierarchy goes from dark to light (base → surface → elevated). In light mode, it reverses (white → light gray → medium gray).
- Don't use pure black (#000) or pure white (#fff) — they create harsh contrast
- Reduce shadow intensity in dark mode (shadows are less visible on dark backgrounds)
- Test all semantic colors against both backgrounds for WCAG compliance
- Use CSS custom properties — theme switching becomes a single class toggle
Lessons Learned
- Start with constraints, not options. Fewer tokens force better decisions.
- Name tokens by function, not appearance. --text-secondary, not --gray-400.
- Every pixel value should trace back to a token. If you're writing a raw number, you're creating debt.
- Design systems are living documents. Audit and prune regularly.
- The best design system is the one your team actually uses.
Building a design system from scratch was the most impactful investment I've made in this portfolio. It slowed me down initially but now every new component is fast, consistent, and visually coherent. If your site looks like a template, your tokens are the first place to look.