Color is one of the most powerful tools in a developer's UI toolkit, yet most of us interact with it by copying hex codes from a design file and calling it a day. Understanding how color models actually work — and how they relate to accessibility standards — transforms you from someone who implements colors to someone who makes informed decisions about them. When a designer hands you #4f46e5, you should be able to reason about what happens when you need a lighter variant, a dark mode equivalent, or a text color that meets contrast requirements.
This guide covers the color models you will encounter in web development, how to convert between them, the WCAG accessibility standards every developer should know, and practical strategies for building color palettes that work.
The Three Color Models You Need to Know
HEX: The Developer Default
Hexadecimal color codes are the format most developers encounter first. A hex color is a six-character string (preceded by #) that encodes the red, green, and blue channels as two-digit hexadecimal values, each ranging from 00 (0) to FF (255).
#4f46e5
││││││
││││└└── Blue: 0xE5 = 229
││└└──── Green: 0x46 = 70
└└────── Red: 0x4F = 79
HEX also supports shorthand notation where each channel is a single character: #fff expands to #ffffff. An optional eighth character (or fourth in shorthand) represents the alpha channel for transparency: #4f46e580 is the same blue at roughly 50% opacity.
HEX is compact and ubiquitous, but it has a major drawback: it is not intuitive. Looking at #4f46e5, you cannot easily tell how bright or saturated the color is, or what hue family it belongs to. This makes manual manipulation — "make this 20% lighter" — essentially guesswork.
RGB: The Machine Model
RGB (Red, Green, Blue) represents the same information as HEX but in decimal notation. In CSS, it uses the rgb() function:
rgb(79, 70, 229) /* opaque */
rgb(79 70 229 / 0.5) /* 50% transparent (modern syntax) */
rgba(79, 70, 229, 0.5) /* 50% transparent (legacy syntax) */
RGB directly models how screens produce color: by mixing red, green, and blue light at varying intensities. When all channels are at 0, you get black; when all are at 255, you get white. Equal values produce grays. This model is perfect for computers but counterintuitive for humans — if someone asks you to "make this blue a bit warmer," adjusting RGB values to achieve that requires mental gymnastics.
HSL: The Human-Friendly Model
HSL (Hue, Saturation, Lightness) describes color the way humans naturally think about it:
- Hue — The base color, represented as an angle on the color wheel from 0° to 360° (0° = red, 120° = green, 240° = blue).
- Saturation — The intensity or purity of the color, from 0% (gray) to 100% (fully vivid).
- Lightness — How light or dark the color is, from 0% (black) to 100% (white), with 50% being the pure color.
hsl(243, 75%, 59%) /* our indigo blue */
hsl(243 75% 59% / 0.5) /* at 50% opacity */
HSL's power is in manipulation. Need a lighter variant? Increase lightness. Need a muted version? Decrease saturation. Need a complementary color? Add 180° to the hue. These operations are trivial in HSL but require complex calculations in RGB or HEX.
/* Base color */
hsl(243, 75%, 59%)
/* Lighter variant for backgrounds */
hsl(243, 75%, 92%)
/* Darker variant for text */
hsl(243, 75%, 35%)
/* Desaturated for disabled states */
hsl(243, 20%, 59%)
/* Complementary color */
hsl(63, 75%, 59%)
Converting Between Color Formats
In practice, you will constantly need to move between HEX, RGB, and HSL. Here is how the conversions work programmatically in JavaScript:
HEX to RGB
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return { r, g, b };
}
hexToRgb("#4f46e5"); // { r: 79, g: 70, b: 229 }
RGB to HSL
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
rgbToHsl(79, 70, 229); // { h: 243, s: 75, l: 59 }
In CSS, modern browsers accept all three formats interchangeably, so conversion is often only needed for programmatic manipulation or when working with design tools that output a specific format.
Convert between HEX, RGB, and HSL instantly with our free Color Picker tool — with live preview and copy-to-clipboard.
Open Color Picker →WCAG Contrast: Making Colors Accessible
The Web Content Accessibility Guidelines (WCAG) define minimum contrast ratios between text and its background to ensure readability for people with low vision or color vision deficiencies. These are not suggestions — they are legal requirements in many jurisdictions and a fundamental part of professional web development.
Understanding Contrast Ratios
Contrast ratio is calculated from the relative luminance of two colors and expressed as a ratio from 1:1 (no contrast, identical colors) to 21:1 (maximum contrast, black on white). The formula uses linearized RGB values to account for how human vision perceives brightness:
// Relative luminance calculation
function luminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
// Contrast ratio
function contrastRatio(rgb1, rgb2) {
const l1 = luminance(...rgb1);
const l2 = luminance(...rgb2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
WCAG Levels and Requirements
- Level AA (minimum) — Normal text (<18px or <14px bold): contrast ratio of at least 4.5:1. Large text (≥18px or ≥14px bold): at least 3:1.
- Level AAA (enhanced) — Normal text: at least 7:1. Large text: at least 4.5:1.
For example, light gray text (#767676) on a white background (#ffffff) has a contrast ratio of exactly 4.54:1 — just barely passing AA for normal text. Anything lighter than that fails. This is why #767676 is sometimes called "the lightest accessible gray."
Common Contrast Mistakes
- Placeholder text that is too light. Input placeholders are frequently styled in very light gray. Even though placeholder text is technically not "real" content, WCAG still requires sufficient contrast for all meaningful text.
- Colored text on colored backgrounds. A saturated blue on a dark navy background might look visually distinct to someone with full color vision but fail contrast checks because both colors have similar luminance values.
- Ignoring dark mode. Colors that pass contrast checks in light mode may fail in dark mode. You need to verify contrast ratios for every theme your application supports.
Building Color Palettes That Work
Armed with HSL understanding and WCAG requirements, here are practical strategies for building color systems:
The Lightness Scale Approach
Pick a base hue and saturation, then generate a scale by varying only the lightness value. This is how systems like Tailwind CSS build their color palettes:
/* Indigo scale */
--indigo-50: hsl(243, 75%, 97%); /* lightest — backgrounds */
--indigo-100: hsl(243, 75%, 93%);
--indigo-200: hsl(243, 75%, 85%);
--indigo-300: hsl(243, 75%, 75%);
--indigo-400: hsl(243, 75%, 65%);
--indigo-500: hsl(243, 75%, 59%); /* base — primary buttons */
--indigo-600: hsl(243, 75%, 50%);
--indigo-700: hsl(243, 75%, 42%);
--indigo-800: hsl(243, 75%, 33%);
--indigo-900: hsl(243, 75%, 25%); /* darkest — text */
Semantic Color Tokens
Rather than using raw color values throughout your codebase, define semantic tokens that describe the color's purpose. This makes theme switching (including dark mode) straightforward:
:root {
--color-primary: hsl(243, 75%, 59%);
--color-primary-hover: hsl(243, 75%, 50%);
--color-text: hsl(243, 20%, 15%);
--color-text-muted: hsl(243, 10%, 45%);
--color-bg: hsl(0, 0%, 100%);
--color-bg-subtle: hsl(243, 30%, 97%);
}
[data-theme="dark"] {
--color-primary: hsl(243, 75%, 68%);
--color-primary-hover: hsl(243, 75%, 75%);
--color-text: hsl(243, 15%, 90%);
--color-text-muted: hsl(243, 10%, 60%);
--color-bg: hsl(243, 20%, 10%);
--color-bg-subtle: hsl(243, 20%, 15%);
}
Testing Your Palette
Before shipping any color palette, run through this checklist:
- Contrast check every text/background pair against WCAG AA minimums.
- Simulate color blindness — Chrome DevTools can simulate protanopia, deuteranopia, and tritanopia. Ensure your UI does not rely solely on color to convey meaning.
- Test in both light and dark mode with actual content, not just swatches.
- Verify on different displays — colors render differently on LCD vs OLED, calibrated vs uncalibrated monitors.
Modern CSS Color: What Is Coming
CSS is evolving beyond sRGB. The oklch() and oklab() color functions offer perceptually uniform color spaces where equal numeric changes produce equal visual changes — unlike HSL, where a 10% lightness change at different hue angles looks dramatically different. The color-mix() function allows blending colors directly in CSS without preprocessors. These features are already supported in all major browsers and represent the future of color on the web.
/* oklch: lightness, chroma, hue */
color: oklch(0.55 0.25 265);
/* Mixing colors in CSS */
background: color-mix(in oklch, var(--primary) 70%, white);
Conclusion
Color in web development is not just about aesthetics — it is about communication, accessibility, and systematic thinking. Understanding the strengths of HEX (compact, universal), RGB (precise, machine-friendly), and HSL (intuitive, manipulable) lets you choose the right format for each context. Meeting WCAG contrast requirements is not optional — it determines whether millions of users can actually read your content. And building a structured color system with semantic tokens saves you from the chaos of ad-hoc color values scattered across a codebase.
The next time you need to pick a color, do not just eyeball it. Use a proper color picker that shows you all three formats, check your contrast ratios, and build your palette systematically from the start.