06
1 min read
Color
Twelve slightly different grays and a blue that drifts across the app. That's not a palette. That's residue.
Build a ramp first: one hue, stepped from 50 to 900. Those steps are facts. Then map semantic roles onto them. primary, background, foreground, border, muted, destructive, success, warning. Components use the roles. They never touch a ramp step.
Contrast is a readability problem, not a token-structure problem. Grade covers the ratios. This lesson is the wiring.
Residue on the left — three leftover blues. Roles on a ramp on the right. Components never touch a step.
Learn more
button#3b82f6
input#2563eb
link#1d4ed8
Learn more
buttonprimary → blue-600
inputprimary → blue-600
linkprimary → blue-600
The leftover blues never agreed. primary points at one step, so every piece does.
:root {
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-300: #93c5fd;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-700: #1d4ed8;
--blue-900: #1e3a8a;
--slate-50: #f8fafc;
--slate-200: #e2e8f0;
--slate-900: #0f172a;
--red-600: #dc2626;
--green-600: #16a34a;
--amber-500: #f59e0b;
--color-primary: var(--blue-600);
--color-background: var(--slate-50);
--color-foreground: var(--slate-900);
--color-border: var(--slate-200);
--color-muted: var(--slate-200);
--color-destructive: var(--red-600);
--color-success: var(--green-600);
--color-warning: var(--amber-500);
}:root {
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-300: #93c5fd;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-700: #1d4ed8;
--blue-900: #1e3a8a;
--slate-50: #f8fafc;
--slate-200: #e2e8f0;
--slate-900: #0f172a;
--red-600: #dc2626;
--green-600: #16a34a;
--amber-500: #f59e0b;
--color-primary: var(--blue-600);
--color-background: var(--slate-50);
--color-foreground: var(--slate-900);
--color-border: var(--slate-200);
--color-muted: var(--slate-200);
--color-destructive: var(--red-600);
--color-success: var(--green-600);
--color-warning: var(--amber-500);
}