Web Color Theory — Color Palettes & Accessibility Guide
In this tutorial, you'll learn about Web Color Theory. We cover key concepts, practical examples, and best practices.
Color theory is the practice of using colour effectively in design — understanding hue, saturation, contrast, and accessibility to create interfaces that are both beautiful and usable by everyone.
What You'll Learn
- The colour wheel and harmonious palette types
- How contrast ratios work and WCAG compliance
- Building accessible colour systems with CSS
Why It Matters
Colour is the first thing users notice — it affects emotion, readability, and brand recognition. 8% of men and 0.5% of women have some form of colour blindness. If your UI relies only on colour to convey information, you exclude millions of users. Accessibility-friendly colour choices improve usability for everyone, especially in bright sunlight or on low-quality screens.
Real-world use: Doda Browser's UI uses a carefully chosen colour palette that works for colour-blind users. Durga Antivirus Pro uses red for critical threats, yellow for warnings, and green for safe — but also adds icons and text labels so colour is not the only differentiator.
Colour Fundamentals
flowchart LR A[Hue] --> B[Saturation] B --> C[Lightness] C --> D[Color Space:
sRGB, HSL, OKLCH] D --> E[Palette
Types] E --> F[Contrast &
Accessibility] style A fill:#f90,color:#fff style C fill:#4af,color:#fff style E fill:#4a4,color:#fff style F fill:#f4a,color:#fff
HSL Colour Model
HSL (Hue, Saturation, Lightness) is the most intuitive way to define colours in CSS:
- Hue: The colour on a 0–360 degree wheel (0=red, 120=green, 240=blue)
- Saturation: Intensity from 0% (grey) to 100% (full colour)
- Lightness: Brightness from 0% (black) to 100% (white)
Palette Types
| Type | How It Works | Best For |
|---|---|---|
| Monochromatic | One hue with varying lightness/ saturation | Clean, minimalist UIs, data visualisation |
| Analogous | 3 adjacent hues on the colour wheel | Calm, harmonious designs |
| Complementary | 2 opposite hues on the colour wheel | High contrast, call-to-action buttons |
| Triadic | 3 evenly spaced hues (120° apart) | Vibrant, playful interfaces |
| Tetradic | 2 complementary pairs (4 colours total) | Complex dashboards, data-rich apps |
Building Accessible Colour Systems
Example 1: Creating a Colour Palette with CSS Custom Properties
/* Accessible color system using HSL */
:root {
/* Primary palette */
--color-primary-h: 222;
--color-primary-s: 89%;
--color-primary-50: hsl(var(--color-primary-h), var(--color-primary-s), 97%);
--color-primary-100: hsl(var(--color-primary-h), var(--color-primary-s), 94%);
--color-primary-200: hsl(var(--color-primary-h), var(--color-primary-s), 86%);
--color-primary-300: hsl(var(--color-primary-h), var(--color-primary-s), 77%);
--color-primary-400: hsl(var(--color-primary-h), var(--color-primary-s), 66%);
--color-primary-500: hsl(var(--color-primary-h), var(--color-primary-s), 50%);
--color-primary-600: hsl(var(--color-primary-h), var(--color-primary-s), 40%);
--color-primary-700: hsl(var(--color-primary-h), var(--color-primary-s), 32%);
--color-primary-800: hsl(var(--color-primary-h), var(--color-primary-s), 24%);
--color-primary-900: hsl(var(--color-primary-h), var(--color-primary-s), 15%);
/* Neutral palette */
--color-neutral-50: #f9fafb;
--color-neutral-100: #f3f4f6;
--color-neutral-200: #e5e7eb;
--color-neutral-300: #d1d5db;
--color-neutral-400: #9ca3af;
--color-neutral-500: #6b7280;
--color-neutral-600: #4b5563;
--color-neutral-700: #374151;
--color-neutral-800: #1f2937;
--color-neutral-900: #111827;
/* Semantic colours */
--color-success: #16a34a;
--color-warning: #d97706;
--color-error: #dc2626;
--color-info: #2563eb;
/* Surface & text */
--color-bg: var(--color-neutral-50);
--color-text: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-500);
--color-border: var(--color-neutral-200);
}
/* Apply the system */
body {
background: var(--color-bg);
color: var(--color-text);
}
.secondary-text {
color: var(--color-text-secondary);
}
.btn-primary {
background: var(--color-primary-500);
color: white;
}
.btn-primary:hover {
background: var(--color-primary-600);
}
Expected output: A consistent colour system applied across the UI. Buttons use var(--color-primary-500) for default state and darken to 600 on hover. Text is near-black (#111827) on near-white (#f9fafb) for maximum readability. Secondary text uses #6b7280 (gray-500) which has a 4.6:1 contrast ratio on white — meeting WCAG AA.
Example 2: Checking Contrast Ratio Programmatically
// Check WCAG contrast ratios in the browser
function calculateContrast(hex1, hex2) {
function luminance(hex) {
const rgb = hex.match(/[A-Fa-f0-9]{2}/g).map(c => {
const v = parseInt(c, 16) / 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
}
const L1 = luminance(hex1);
const L2 = luminance(hex2);
const ratio = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
return ratio;
}
// Test colour combinations
const tests = [
{ fg: '#111827', bg: '#f9fafb', label: 'Dark text on light bg' },
{ fg: '#6b7280', bg: '#ffffff', label: 'Gray-500 on white' },
{ fg: '#9ca3af', bg: '#ffffff', label: 'Gray-400 on white' },
{ fg: '#ffffff', bg: '#2563eb', label: 'White on blue-600' },
{ fg: '#dc2626', bg: '#ffffff', label: 'Red-600 on white' },
];
tests.forEach(({ fg, bg, label }) => {
const ratio = calculateContrast(fg, bg);
const aa = ratio >= 4.5 ? '✅ AA' : '❌ Fail';
const aaa = ratio >= 7 ? '(AAA ✅)' : '(AAA ❌)';
console.log(`${label}: ${ratio.toFixed(2)}:1 — ${aa} ${aaa}`);
});
Expected output:
Dark text on light bg: 15.05:1 — ✅ AA (AAA ✅)
Gray-500 on white: 4.62:1 — ✅ AA (AAA ❌)
Gray-400 on white: 3.23:1 — ❌ Fail (AAA ❌)
White on blue-600: 8.59:1 — ✅ AA (AAA ✅)
Red-600 on white: 4.08:1 — ❌ Fail (AAA ❌)
Note that Gray-400 (#9ca3af) and Red-600 (#dc2626) both fail WCAG AA for normal text. You'd need darker shades or larger text sizes to use these colours.
Example 3: OKLCH for Perceptually Uniform Colour
/* Modern browsers support OKLCH — perceptually uniform color space */
:root {
/* OKLCH ensures consistent perceived lightness across hues */
--blue: oklch(55% 0.2 250);
--green: oklch(55% 0.2 145);
--red: oklch(55% 0.2 30);
--yellow: oklch(55% 0.2 90);
--purple: oklch(55% 0.2 290);
/* Why OKLCH matters:
In HSL, yellow at 55% lightness looks much brighter than blue at 55%.
In OKLCH, 55% lightness looks equally bright across all hues.
*/
}
.demo-colors {
display: flex;
gap: 1rem;
}
.swatch {
width: 100px;
height: 100px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.swatch-blue { background: var(--blue); }
.swatch-green { background: var(--green); }
.swatch-red { background: var(--red); }
.swatch-yellow { background: var(--yellow); }
.swatch-purple { background: var(--purple); }
Expected output: All five swatches appear equally bright despite being different hues. In HSL, the yellow would appear significantly brighter and the blue much darker. OKLCH solves this perceptual non-uniformity, making it easier to build balanced colour palettes.
WCAG Colour Requirements
| Level | Normal Text | Large Text (18px+ bold or 24px+) | UI Components |
|---|---|---|---|
| AA | 4.5:1 | 3:1 | 3:1 |
| AAA | 7:1 | 4.5:1 | Not required |
Note: Colour should never be the only way to convey information. Always include icons, text labels, or patterns alongside colour.
Common Colour Errors
Red-green colour reliance — Using red/green as the only differentiator (e.g., "items in red are blocked, green are safe"). 8% of men can't distinguish them. Add icons like a lock for blocked and shield for safe.
Low contrast text — Light grey text (#999, #aaa) is popular but fails WCAG on white backgrounds. Always check contrast ratio. Use tools like the WebAIM Contrast Checker during design.
Inconsistent saturation across a palette — A primary blue at 100% saturation next to a muted grey at 20% saturation creates visual imbalance. Keep saturation consistent within each tier of your palette.
Not providing dark mode colours — Users in dark environments or with visual sensitivities prefer dark themes. Use
prefers-color-scheme: darkmedia query and ensure all colour pairs meet contrast requirements in both modes.Using colour alone for link identification — Underlined links or different font weight help users distinguish links from regular text without relying on colour. WCAG requires links to be identifiable by more than colour.
Overly vibrant background colours — Bright saturated backgrounds cause eye strain and make text unreadable. Use low-saturation, high-lightness variants for backgrounds (like
hsl(222, 20%, 97%)instead of pure blue).
Frequently Asked Questions
Practice Questions
What is the difference between HSL and OKLCH colour spaces? HSL uses a cylindrical model that doesn't account for human perception — 50% lightness yellow looks brighter than 50% lightness blue. OKLCH is perceptually uniform: equal lightness values look equally bright across all hues.
What contrast ratio does WCAG AA require for normal text? 4.5:1 minimum. Large text (18px+ bold or 24px+) needs 3:1. AAA requires 7:1 for normal text and 4.5:1 for large text.
Why should you not use colour alone to convey information? Colour-blind users can't distinguish certain colour pairs. Additionally, screen readers don't convey colour. Always use icons, text labels, or patterns alongside colour.
What is a monochromatic colour palette? A palette built from a single hue at varying lightness and saturation levels. It creates a cohesive, minimal look and is easy to make accessible because contrast is predictable.
How do you implement dark mode with CSS custom properties? Define colour variables in
:rootfor light mode, then override them inside@media (prefers-color-scheme: dark) { :root { ... } }. Use semantic variable names like--color-bginstead of--color-white.
Challenge
Build a complete accessible colour system for DodaTech: create 10-shade palettes for primary, success, warning, and error colours using HSL. Verify every shade pair against WCAG AA and AAA for text and large text. Add OKLCH equivalents for modern browsers. Create utility classes for text, background, border, and hover states. Test with a colour-blindness simulator (like Chrome DevTools rendering emulation).
Real-World Task
Durga Antivirus Pro's threat dashboard uses colour-coded severity badges: red for critical, orange for high, yellow for medium, green for low. A user base audit reveals 12% of enterprise customers use accessibility tools. Redesign the badges to include icons (skull, exclamation, warning, shield) and ensure every badge passes WCAG AA for both the badge text and the background colour contrast. Add a high-contrast mode toggle that switches to a text-only representation.
Related: Web Accessibility Guide | Related: CSS Preprocessors | Related: Responsive Design Guide
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro