Choose and implement colors intentionally — build accessible palettes using HSL, understand color psychology for UI, implement semantic color tokens for status and feedback, and support dark mode with minimal code duplication.
Color is the most emotionally powerful design tool. It can direct attention, communicate status, evoke trust, and signal action. It can also confuse, exclude, and distract when used without intention.
For developers, color decisions have two dimensions: semantic (what does this color communicate?) and technical (does it meet accessibility requirements?). Understanding both dimensions lets you evaluate colors critically and implement them correctly.
This lesson covers the theory behind intentional color use and the CSS techniques for building accessible, maintainable color systems.
HSL (Hue, Saturation, Lightness) is a more intuitive color model than hex or RGB for developers. It lets you create palette scales by changing only the Lightness value while keeping Hue and Saturation consistent.
HSL syntax:
1/* hsl(hue, saturation, lightness) */
2hsl(220, 90%, 56%) /* blue at 220 degrees, high saturation, medium lightness */
3hsl(142, 71%, 45%) /* green at 142 degrees, medium-high saturation */
4hsl(0, 0%, 100%) /* white — no saturation, maximum lightness */
5hsl(0, 0%, 0%) /* black — no saturation, zero lightness */Building a complete blue palette with HSL:
1:root {
2 /* Blue palette: same hue (220) and saturation (90%), only lightness varies */
3 --blue-50: hsl(220, 90%, 96%); /* #eff6ff — very light */
4 --blue-100: hsl(220, 90%, 91%); /* #dbeafe */
5 --blue-200: hsl(220, 90%, 82%); /* #bfdbfe */
6 --blue-300: hsl(220, 90%, 71%); /* #93c5fd */
7 --blue-400: hsl(220, 80%, 63%); /* #60a5fa */
8 --blue-500: hsl(220, 90%, 56%); /* #3b82f6 */
9 --blue-600: hsl(220, 90%, 47%); /* #2563eb — primary action */
10 --blue-700: hsl(220, 90%, 40%); /* #1d4ed8 — hover state */
11 --blue-800: hsl(220, 90%, 34%); /* #1e40af */
12 --blue-900: hsl(220, 90%, 25%); /* #1e3a8a — very dark */
13}Building a neutral gray palette with HSL (very slight blue tint reads as 'cool'):
1:root {
2 /* Cool grays: slight blue tint (220) makes them feel modern */
3 --gray-50: hsl(220, 14%, 96%); /* #f8fafc */
4 --gray-100: hsl(220, 14%, 92%); /* #f1f5f9 */
5 --gray-200: hsl(220, 13%, 91%); /* #e2e8f0 */
6 --gray-300: hsl(220, 11%, 83%); /* #cbd5e1 */
7 --gray-400: hsl(220, 9%, 64%); /* #94a3b8 */
8 --gray-500: hsl(220, 9%, 46%); /* #64748b */
9 --gray-600: hsl(220, 13%, 36%); /* #475569 */
10 --gray-700: hsl(220, 15%, 27%); /* #334155 */
11 --gray-800: hsl(220, 18%, 20%); /* #1e293b */
12 --gray-900: hsl(220, 22%, 10%); /* #0f172a */
13}Modern CSS color syntax (oklch — more perceptually uniform):
1/* oklch(lightness, chroma, hue) — better for perceptually uniform palettes */
2--blue-600: oklch(0.52 0.2 264); /* perceptually consistent blue */
3--green-600: oklch(0.52 0.2 160); /* same lightness = same visual weight */
4/* With oklch: two colors at the same lightness value look equally bright */
5/* With hsl: different hues at same lightness value look different brightness */Semantic colors communicate meaning consistently. Every error in the product should use the same red. Every success should use the same green. This creates a visual language users learn once and apply everywhere.
Standard semantic color hues:
1Success/Positive → Green (~120-160 hue)
2Warning/Caution → Amber/Yellow (~30-50 hue)
3Error/Danger → Red (~0-20 hue)
4Info/Neutral → Blue (~200-240 hue)Semantic color token system:
1/* Primitive colors */
2:root {
3 /* Green for success */
4 --green-50: hsl(143, 71%, 96%);
5 --green-100: hsl(143, 71%, 88%);
6 --green-600: hsl(143, 71%, 37%); /* #16a34a */
7 --green-700: hsl(143, 71%, 32%); /* hover */
8
9 /* Amber for warning */
10 --amber-50: hsl(48, 96%, 95%);
11 --amber-500: hsl(38, 92%, 50%); /* #f59e0b */
12 --amber-700: hsl(32, 91%, 39%);
13
14 /* Red for error/danger */
15 --red-50: hsl(0, 85%, 97%);
16 --red-100: hsl(0, 93%, 93%);
17 --red-600: hsl(0, 72%, 51%); /* #dc2626 */
18 --red-700: hsl(0, 74%, 42%);
19
20 /* Blue for info (reuse primary blue palette) */
21}
22
23/* Semantic layer */
24:root {
25 --color-success: var(--green-600);
26 --color-success-subtle: var(--green-50);
27 --color-warning: var(--amber-500);
28 --color-warning-subtle: var(--amber-50);
29 --color-danger: var(--red-600);
30 --color-danger-subtle: var(--red-50);
31 --color-info: var(--blue-600);
32 --color-info-subtle: var(--blue-50);
33}
34
35/* Application */
36.alert-success {
37 background-color: var(--color-success-subtle);
38 border-left: 4px solid var(--color-success);
39 color: var(--green-700); /* darker for text legibility */
40}
41
42.alert-error {
43 background-color: var(--color-danger-subtle);
44 border-left: 4px solid var(--color-danger);
45 color: var(--red-700);
46}Accessibility rule: color is never the ONLY indicator
1/* BAD: Error shown by red color only — fails for color-blind users */
2.input-error { border-color: var(--color-danger); }
3
4/* GOOD: Error shown by color + icon + text */
5.input-error {
6 border-color: var(--color-danger);
7}
8.input-error-message {
9 display: flex;
10 align-items: center;
11 gap: var(--space-1);
12 color: var(--color-danger);
13 font-size: 0.875rem;
14}
15.input-error-icon {
16 /* Supplemental shape indicator */
17 width: 1rem;
18 height: 1rem;
19 flex-shrink: 0;
20}Dark mode requires a different approach than just inverting colors. True dark mode uses a separate carefully-chosen set of values that provide good contrast on dark backgrounds.
Dark mode mistakes:
Dark mode best practices:
CSS implementation:
1/* Light mode semantic tokens (default) */
2:root {
3 --color-bg-page: var(--gray-0, #ffffff);
4 --color-bg-surface: var(--gray-50, #f9fafb);
5 --color-text: var(--gray-900, #111827);
6 --color-text-muted: var(--gray-500, #6b7280);
7 --color-border: var(--gray-200, #e5e7eb);
8 --color-action: var(--blue-600, #2563eb);
9}
10
11/* Dark mode: only change semantic tokens */
12@media (prefers-color-scheme: dark) {
13 :root {
14 --color-bg-page: hsl(220, 22%, 10%); /* #0f172a — deep dark */
15 --color-bg-surface: hsl(220, 18%, 14%); /* #1e293b — card surfaces */
16 --color-text: hsl(220, 14%, 96%); /* #f8fafc — near-white */
17 --color-text-muted: hsl(220, 9%, 64%); /* #94a3b8 — muted gray */
18 --color-border: hsl(220, 15%, 27%); /* #334155 */
19 --color-action: hsl(220, 80%, 63%); /* #60a5fa — lighter blue for dark bg */
20 /* Note: action lightened from 47% to 63% for dark background legibility */
21 }
22}
23
24/* Class-based (JavaScript toggle) */
25[data-theme='dark'] {
26 --color-bg-page: hsl(220, 22%, 10%);
27 --color-text: hsl(220, 14%, 96%);
28 /* ... same as @media block */
29}
30
31/* Components work without changes: */
32.card {
33 background-color: var(--color-bg-surface); /* light: white; dark: dark-gray */
34 border: 1px solid var(--color-border); /* light: light-gray; dark: dark-gray */
35 color: var(--color-text); /* light: dark; dark: light */
36}
37
38/* Zero component changes for dark mode — only tokens change */Color psychology in UI is not mystical — it is based on cultural conventions and perceptual associations that are consistent enough to be predictable.
Color associations in digital products:
1Blue:
2 Associations: trust, professionalism, technology, calm
3 Common uses: primary actions, links, information states
4 Avoid: danger indicators (confusable with info)
5
6Green:
7 Associations: success, growth, go, positive, nature
8 Common uses: success states, enrollment confirmations, active states
9 Avoid: prices (may look like a discount when not intentional)
10
11Red:
12 Associations: error, danger, stop, urgency, attention
13 Common uses: error states, destructive actions, urgent notifications
14 Avoid: primary CTAs (creates anxiety unless urgency is intended)
15
16Amber/Orange:
17 Associations: warning, caution, energy, warmth
18 Common uses: warning states, featured/promotional content, CTAs (warmth)
19 Avoid: text on white (many amber shades fail WCAG contrast)
20
21Gray:
22 Associations: neutral, professional, disabled, secondary
23 Common uses: body text, backgrounds, disabled states, secondary content
24 Avoid: primary CTAs (implies inaction)
25
26Purple:
27 Associations: premium, creative, luxury, mystery
28 Common uses: premium tier indicators, creative platform branding
29 Avoid: error/warning (confusable with red/amber)Limiting the color palette:
1Rule of thumb: 60-30-10
2 60% — Primary neutral (background, surface, body text)
3 30% — Secondary neutral (cards, borders, muted text)
4 10% — Accent (action buttons, links, key highlights)
5
6For a product like DevBootstrap:
7 60% — Gray scale (backgrounds, body text)
8 30% — Blue scale (action color, links, information)
9 10% — Green/Red/Amber (success, error, warning — only when needed)
10
11Anti-pattern: using many accent colors
12 Avoid: blue primary, purple secondary, orange highlights, green badges
13 Better: blue primary with semantic greens/reds/ambers only for statusColor audit — finding unnecessary colors:
1/* Find all unique color values in the codebase */
2/* Command: grep -oE '#[0-9a-fA-F]{6}' src/styles/**/*.css | sort -u */
3/* If more than 20 unique colors: color system is not enforced */
4
5/* Target: 10-15 unique colors total for most products */
6/* 9 in gray scale + 5 in blue scale + 3 semantic = 17 — reasonable */Implementing the complete color token system for DevBootstrap using HSL primitives, semantic tokens, and dark mode support.
1/* DEVBOOTSTRAP COLOR SYSTEM */
2/* Built with HSL primitives + semantic tokens + dark mode */
3
4/* ============================================
5 PRIMITIVE TOKENS: Never use directly in components
6 ============================================ */
7:root {
8 /* Primary blue: hue 220, saturation varies for vibrancy */
9 --prim-blue-50: hsl(213, 100%, 96%); /* #eff6ff */
10 --prim-blue-100: hsl(213, 93%, 90%); /* #dbeafe */
11 --prim-blue-400: hsl(213, 91%, 63%); /* #60a5fa */
12 --prim-blue-500: hsl(213, 91%, 56%); /* #3b82f6 */
13 --prim-blue-600: hsl(221, 83%, 53%); /* #2563eb — primary action */
14 --prim-blue-700: hsl(224, 76%, 48%); /* #1d4ed8 — hover */
15 --prim-blue-800: hsl(226, 71%, 40%); /* #1e40af */
16
17 /* Success green: hue 142 */
18 --prim-green-50: hsl(142, 76%, 94%);
19 --prim-green-400: hsl(142, 71%, 45%);
20 --prim-green-600: hsl(142, 71%, 37%); /* #16a34a */
21 --prim-green-700: hsl(142, 72%, 30%);
22
23 /* Error red: hue 0 */
24 --prim-red-50: hsl(0, 86%, 97%);
25 --prim-red-400: hsl(0, 91%, 71%);
26 --prim-red-600: hsl(0, 72%, 51%); /* #dc2626 */
27 --prim-red-700: hsl(0, 74%, 42%);
28
29 /* Warning amber: hue 38 */
30 --prim-amber-50: hsl(48, 96%, 95%);
31 --prim-amber-400: hsl(43, 96%, 56%);
32 --prim-amber-500: hsl(38, 92%, 50%); /* #f59e0b */
33 --prim-amber-700: hsl(32, 91%, 39%);
34
35 /* Cool neutrals: hue 220 for slight blue tint */
36 --prim-gray-0: hsl(0, 0%, 100%); /* #ffffff */
37 --prim-gray-50: hsl(210, 20%, 98%); /* #f9fafb */
38 --prim-gray-100: hsl(220, 14%, 96%);
39 --prim-gray-200: hsl(220, 13%, 91%); /* #e5e7eb */
40 --prim-gray-300: hsl(220, 11%, 84%);
41 --prim-gray-400: hsl(220, 9%, 64%); /* #9ca3af */
42 --prim-gray-500: hsl(220, 9%, 46%); /* #6b7280 */
43 --prim-gray-600: hsl(220, 13%, 36%);
44 --prim-gray-700: hsl(220, 15%, 27%); /* #374151 */
45 --prim-gray-800: hsl(220, 18%, 20%);
46 --prim-gray-900: hsl(220, 22%, 10%); /* #111827 */
47}
48
49/* ============================================
50 SEMANTIC TOKENS: Use these in all components
51 Light mode (default)
52 ============================================ */
53:root {
54 /* Backgrounds */
55 --color-bg-page: var(--prim-gray-0);
56 --color-bg-surface: var(--prim-gray-50);
57 --color-bg-elevated: var(--prim-gray-0);
58
59 /* Text */
60 --color-text: var(--prim-gray-900);
61 --color-text-muted: var(--prim-gray-500);
62 --color-text-disabled: var(--prim-gray-400);
63 --color-text-inverse: var(--prim-gray-0);
64
65 /* Borders */
66 --color-border: var(--prim-gray-200);
67 --color-border-strong: var(--prim-gray-300);
68 --color-border-focus: var(--prim-blue-600);
69
70 /* Action */
71 --color-action: var(--prim-blue-600);
72 --color-action-hover: var(--prim-blue-700);
73 --color-action-subtle: var(--prim-blue-50);
74
75 /* Status */
76 --color-success: var(--prim-green-600);
77 --color-success-subtle: var(--prim-green-50);
78 --color-danger: var(--prim-red-600);
79 --color-danger-subtle: var(--prim-red-50);
80 --color-warning: var(--prim-amber-500);
81 --color-warning-subtle: var(--prim-amber-50);
82}
83
84/* ============================================
85 DARK MODE: Only reassign semantic layer
86 Primitive tokens unchanged
87 ============================================ */
88@media (prefers-color-scheme: dark) {
89 :root {
90 --color-bg-page: hsl(220, 22%, 8%); /* deep dark */
91 --color-bg-surface: hsl(220, 18%, 13%);
92 --color-bg-elevated: hsl(220, 15%, 17%);
93
94 --color-text: var(--prim-gray-50);
95 --color-text-muted: var(--prim-gray-400);
96 --color-text-disabled: var(--prim-gray-600);
97
98 --color-border: hsl(220, 15%, 22%);
99 --color-border-strong: hsl(220, 13%, 28%);
100
101 --color-action: var(--prim-blue-400); /* lighter blue for dark bg */
102 --color-action-hover: hsl(213, 85%, 70%);
103 --color-action-subtle: hsl(221, 60%, 18%);
104 }
105}
106
107/* USAGE EXAMPLE: Component uses only semantic tokens */
108.card {
109 background-color: var(--color-bg-elevated);
110 border: 1px solid var(--color-border);
111 /* Automatic light/dark without any component changes */
112}
113
114.card-title { color: var(--color-text); }
115.card-meta { color: var(--color-text-muted); }
116.card-cta { background-color: var(--color-action); color: var(--color-text-inverse); }The color system uses HSL throughout — blue palette is hue 213-221 with varying saturation and lightness; green is hue 142; red is hue 0; amber is hue 38. The dark mode block only reassigns semantic tokens — primitives (--prim-*) stay identical. The action color shifts from blue-600 (lightness 53%) to blue-400 (lightness 63%) for dark mode because lighter blues are more visible on dark backgrounds. Components use only semantic tokens — zero component changes are needed for dark mode.
The next lesson covers Typography Essentials — choosing type scales, font pairing, line length, and line height for optimal readability in web interfaces.
Your progress is saved in this browser, on this device only.