100% Private

Color Theory for Web Designers

The difference between a design that looks professional and one that looks amateur often comes down to color. Here's the theory you need, from how digital color works to accessibility requirements to building a palette that actually holds together.

Color Models: RGB, HSL, CMYK, HEX

Color is a perception — your brain's interpretation of light hitting your retina. Digital color systems are models that approximate this experience. The model you use depends on the medium.

Screens emit light, combining it to create colors (additive color mixing). Paint and ink absorb light, subtracting it (subtractive color mixing). These two physics lead to fundamentally different color models.

ModelTypeUsed byRange
RGBAdditiveScreens, web, photos0–255 per channel
HEXAdditive (RGB)Web/CSS00–FF per channel
HSL / HSBAdditive (derived)Design tools, CSS0–360°, 0–100%, 0–100%
CMYKSubtractivePrint, offset press0–100% per channel

As a web designer, you'll live in RGB, HEX, and HSL. CMYK becomes relevant if you're designing assets that will also be printed — the conversion from RGB to CMYK can shift colors noticeably, and some vivid screen colors (especially saturated blues and greens) simply can't be reproduced in CMYK print.

HEX: The Web Standard

HEX is just RGB expressed in base-16 (hexadecimal). Each pair of hex digits maps to one RGB channel: red, green, blue. Digits go from 0–9 then A–F, so the range is 00 (decimal 0) to FF (decimal 255).

#RRGGBB

#FF0000 = rgb(255, 0, 0) = pure red #00FF00 = rgb(0, 255, 0) = pure green #0000FF = rgb(0, 0, 255) = pure blue #FFFFFF = rgb(255,255,255) = white #000000 = rgb(0, 0, 0) = black #808080 = rgb(128,128,128) = medium gray

Shorthand — only when both digits in each pair match: #F00 = #FF0000 (red) #ABC = #AABBCC

With Alpha Transparency

<code">#RRGGBBAA  (8-digit hex — CSS Color Level 4)

#FF0000FF = fully opaque red #FF000080 = 50% transparent red (80 hex = 128 decimal ≈ 50%) #FF000000 = fully transparent red

Manual HEX to RGB Conversion

<code">#4A90E2

Split: 4A | 90 | E2 Convert each hex pair to decimal: 4A = 4×16 + 10 = 74 90 = 9×16 + 0 = 144 E2 = 14×16 + 2 = 226

Result: rgb(74, 144, 226)

Do this instantly with our HEX to RGB Converter.

RGB and When to Use It

RGB expresses color as three values (red, green, blue) each from 0 to 255. It maps directly to how monitors work — each pixel has red, green, and blue subpixels lit at the specified intensities.

<code">rgb(255, 87, 51)        /* A warm orange /
rgba(255, 87, 51, 0.5)  / Same, 50% transparent */

/* Modern CSS syntax (no commas) — CSS Color Level 4 */ rgb(255 87 51) rgb(255 87 51 / 50%) rgb(255 87 51 / 0.5)

RGB is most useful when you're working with color values programmatically — JavaScript animations, canvas drawing, image processing. It's less intuitive for choosing colors by hand, because the relationship between the three numbers and the resulting color isn't obvious. Adjusting brightness requires changing all three values in proportion.

<code">// JavaScript: animate a color transition
function interpolateRGB(from, to, t) {
return {
r: Math.round(from.r + (to.r - from.r) * t),
g: Math.round(from.g + (to.g - from.g) * t),
b: Math.round(from.b + (to.b - from.b) * t),
};
}

// From red to blue over time const color = interpolateRGB({r:255,g:0,b:0}, {r:0,g:0,b:255}, 0.5); // {r: 128, g: 0, b: 128} — purple at midpoint

HSL: The Designer's Format

HSL (Hue, Saturation, Lightness) represents color the way humans think about it:

  • Hue (0°–360°): The color itself — where you are on the color wheel
  • Saturation (0%–100%): How vivid the color is. 0% is gray, 100% is fully saturated
  • Lightness (0%–100%): How light or dark. 0% is black, 100% is white, 50% is the "pure" color

<code">hsl(0, 100%, 50%)     /* Red /
hsl(120, 100%, 50%)   / Green /
hsl(240, 100%, 50%)   / Blue /
hsl(0, 0%, 50%)       / Gray (any hue, 0 saturation) /
hsl(0, 0%, 100%)      / White /
hsl(0, 0%, 0%)        / Black */

hsla(220, 80%, 50%, 0.7) /* Blue, 70% opaque */

/* Modern CSS (no commas) */ hsl(220 80% 50%) hsl(220 80% 50% / 70%)

Hue Map (Color Wheel Positions)

<code">  0° / 360° = Red
30°         = Orange
60°         = Yellow
120°         = Green
180°         = Cyan
210°         = Sky blue
240°         = Blue
270°         = Purple / Violet
300°         = Magenta
330°         = Pink

The power of HSL for design work: you can create a full set of color variants by changing just one value.

<code">/* A blue with all its variants — same hue, different S/L /
hsl(220, 80%, 20%)   / dark navy /
hsl(220, 80%, 35%)   / darker blue /
hsl(220, 80%, 50%)   / pure blue (brand color) /
hsl(220, 80%, 65%)   / light blue /
hsl(220, 80%, 80%)   / pale blue /
hsl(220, 40%, 50%)   / muted/desaturated blue /
hsl(220, 20%, 90%)   / near-white blue tint */

That's a complete single-color palette with no guesswork. With RGB, you'd have to calculate all those by trial and error.

CMYK: Screen vs Print

CMYK (Cyan, Magenta, Yellow, Key/Black) is the ink model used in printing. Mix all four inks at maximum and you get black (theoretically — in practice, printers add the K channel for a truer black). Mix none and you get white (the paper).

CSS has no native CMYK support — browsers don't use it. CMYK matters when your web assets also go to print: logos, marketing materials, product packaging. The RGB-to-CMYK conversion can shift colors, particularly:

  • Vivid greens (RGB can hit 0,255,0 — no equivalent CMYK)
  • Electric blues (impossible in CMYK gamut)
  • Bright oranges (partially out of gamut)

If your brand color is specified in CMYK for print, get the closest-matching RGB value and use that for screen. Don't auto-convert — the math rarely produces a visually equivalent result. A human designer should sign off on the screen-to-print translation.

Convert between formats with our CMYK to HEX Converter.

Color Wheel and Harmony Rules

The color wheel is a circular arrangement of hues. Color harmony rules describe which positions on the wheel produce pleasing combinations. These aren't laws — they're starting points. Great design breaks them intentionally; bad design breaks them accidentally.

Complementary (180° apart)

Blue + Orange. High contrast, energetic. Use sparingly — too much complementary is visually jarring. Best for CTAs against a single dominant color background.

Analogous (30–60° adjacent)

Cyan + Blue + Purple. Harmonious and calm. Works well for monochromatic-ish designs that still have some variation. Nature uses analogous colors constantly.

Triadic (120° apart)

Red + Green + Blue (the primary triad). Vibrant and balanced. Let one color dominate (60%), use the others as secondary (30%) and accent (10%).

Split-Complementary

Base + two colors flanking its complement. Strong contrast but less tension than pure complementary. A safer choice when complementary feels too aggressive.

Generating Harmonies with HSL

<code">/* Given a base hue of 220° (blue) */

/* Complementary: rotate 180° / hsl(220, 80%, 50%) / base / hsl(40, 80%, 50%) / complement (220 + 180 = 400 → 40°) */

/* Analogous: ±30° */ hsl(190, 70%, 50%) hsl(220, 80%, 50%) hsl(250, 70%, 50%)

/* Triadic: +120° each / hsl(220, 80%, 50%) / base / hsl(340, 80%, 50%) / 220 + 120 = 340° (pink) / hsl(100, 80%, 50%) / 220 + 240 = 460 → 100° (yellow-green) */

Generate and visualize color harmonies with our Color Picker.

Contrast Ratios and WCAG

Contrast ratio is the ratio of luminance between two colors. It ranges from 1:1 (identical colors) to 21:1 (black on white). The Web Content Accessibility Guidelines (WCAG) define minimum requirements.

WCAG LevelNormal text (<18pt)Large text (≥18pt bold)UI components
AA (minimum)4.5:13:13:1
AAA (enhanced)7:14.5:1

AA is the legal minimum in most jurisdictions that reference WCAG. AAA is aspirational — worth targeting for body text on sites where accessibility is critical (government, healthcare, financial).

How Contrast Is Calculated

<code">// Relative luminance formula (WCAG 2.x)
function relativeLuminance(r, g, b) {
const toLinear = (c) => {
c = c / 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}

function contrastRatio(rgb1, rgb2) { const L1 = relativeLuminance(...rgb1); const L2 = relativeLuminance(...rgb2); const lighter = Math.max(L1, L2); const darker = Math.min(L1, L2); return (lighter + 0.05) / (darker + 0.05); }

// Example: navy text on white background contrastRatio([0, 31, 63], [255, 255, 255]) // Returns: ~14.7 — well above 4.5:1 AA requirement

Check your color combinations with our Contrast Checker, which shows WCAG compliance at AA and AAA levels.

Common Contrast Failures

  • Light gray text on white background — design trend that fails accessibility
  • Colored text on colored background — hard to predict without checking
  • Ghost buttons — white/light border text on white background
  • Disabled state indicators — often too low contrast by default
  • Placeholder text — browsers default to 40% opacity, which often fails

Color Blindness Considerations

Approximately 8% of men and 0.5% of women have some form of color vision deficiency. Designing for them isn't charity — it's reaching 300+ million people globally.

Types of Color Blindness

TypeDeficiencyPrevalence (men)What's confused
DeuteranomalyReduced green sensitivity~5%Red / green
ProtanomalyReduced red sensitivity~1%Red / green (red appears darker)
DeuteranopiaNo green cones~1%Red / green (severe)
ProtanopiaNo red cones~1%Red / green (severe)
TritanopiaNo blue cones<0.01%Blue / yellow
AchromatopsiaNo color vision0.003%Everything (grayscale only)

Red-green confusion is by far the most common — nearly all color blindness falls into this category. This has direct design implications.

Design Rules

Never use color as the only signal. "Errors are shown in red" fails for red-green color blindness. Add an icon (✕), a text label ("Error:"), a different shape, or a border. Color should be a secondary signal, not the primary one.

Avoid red/green combinations. Red on green or green on red — even if the contrast ratio technically passes — can be impossible to read for deuteranopes. Use red on white or green on dark for status indicators.

Test with a simulator. Browser extensions like Colorblindly let you see your design as someone with each type of color blindness would see it. It's a quick and humbling exercise.

The safe palette: Blue and orange are the most reliably distinguishable across all forms of color blindness. Use blue for one state and orange (or yellow) for another when you need two distinct colors to carry meaning.

<code">/* Don't do this — color only */
.status-success { color: green; }
.status-error   { color: red; }

/* Do this — color + icon + text / .status-success::before { content: "✓ "; } .status-error::before { content: "✕ "; } .status-success { color: hsl(145, 60%, 35%); } / Dark green — good contrast / .status-error { color: hsl(0, 70%, 45%); } / Dark red — good contrast */

Dark Mode Design

Dark mode is now a baseline expectation. Users who prefer it are passionate about it, and implementing it badly (white-on-black text that glows, oversaturated accent colors) is worse than not doing it.

Dark Mode Mistakes

  • Pure black backgrounds (#000000): Creates extreme contrast that causes eye strain and halation on OLED screens. Use very dark gray like hsl(220, 10%, 10%) instead.
  • Same saturated colors in dark mode: Bright blues and reds that look fine on white look aggressive on dark backgrounds. Reduce saturation by 10–20% or increase lightness slightly.
  • Inverting light mode colors: Just flipping light/dark produces ugly, high-contrast results. Redesign for dark, don't invert.
  • Hard shadows on dark: Box shadows invisible. Use glows (matching the element color) or elevation via lighter backgrounds instead.

CSS Implementation

<code">/* System preference /
@media (prefers-color-scheme: dark) {
:root {
--background: hsl(220, 15%, 10%);
--surface: hsl(220, 12%, 15%);
--surface-raised: hsl(220, 10%, 20%);
--text-primary: hsl(220, 10%, 95%);
--text-secondary: hsl(220, 8%, 65%);
--accent: hsl(220, 70%, 65%);    / lighter in dark mode */
--border: hsl(220, 15%, 25%);
}
}

/* Manual toggle — add class to root / :root[data-theme="dark"] { / same variables */ }

/* Both system + manual / @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { / dark mode variables */ } }

Surface Hierarchy in Dark Mode

In light mode, elevation is expressed with shadows. In dark mode, lighter colors indicate higher elevation (closer to the "light source" above). This is Material Design's approach and it works well.

<code">/* Dark mode elevation via lightness /
--surface-level-0: hsl(220, 15%, 8%);   / background /
--surface-level-1: hsl(220, 13%, 12%);  / cards /
--surface-level-2: hsl(220, 11%, 16%);  / modals, popovers /
--surface-level-3: hsl(220, 9%,  20%);  / tooltips, dropdowns */

Building a Palette

A production-ready palette needs more than a few brand colors. You need variants for all states — hover, active, disabled, success, warning, error — across both light and dark modes.

Start with Your Brand Color in HSL

If you're given a HEX brand color, convert it to HSL and use the hue as your anchor. Then build a scale.

<code">/* Brand color: #3b82f6 = hsl(217, 91%, 60%) */

/* Neutral scale (your main grays) */ --gray-50: hsl(220, 14%, 96%); --gray-100: hsl(220, 13%, 91%); --gray-200: hsl(220, 11%, 82%); --gray-300: hsl(216, 12%, 70%); --gray-400: hsl(218, 11%, 58%); --gray-500: hsl(220, 9%, 46%); --gray-600: hsl(215, 14%, 34%); --gray-700: hsl(217, 19%, 27%); --gray-800: hsl(215, 28%, 17%); --gray-900: hsl(221, 39%, 11%);

/* Brand (primary) scale / --blue-50: hsl(214, 100%, 97%); --blue-100: hsl(214, 95%, 93%); --blue-200: hsl(213, 97%, 87%); --blue-300: hsl(212, 96%, 78%); --blue-400: hsl(213, 94%, 68%); --blue-500: hsl(217, 91%, 60%); / brand color */ --blue-600: hsl(221, 83%, 53%); --blue-700: hsl(224, 76%, 48%); --blue-800: hsl(226, 71%, 40%); --blue-900: hsl(224, 64%, 33%);

/* Semantic colors */ --success: hsl(142, 71%, 45%); --warning: hsl(38, 92%, 50%); --error: hsl(0, 84%, 60%);

The 60-30-10 Rule

Use your palette with intention: 60% dominant (typically neutrals — backgrounds, large surfaces), 30% secondary (brand color for key UI elements), 10% accent (CTAs, highlights, notifications). More variety than this creates visual noise.

CSS Custom Properties for Theming

CSS custom properties (variables) are the right tool for color theming. Define your palette once, reference it everywhere, swap themes by changing the root variables.

<code">/* Semantic layer — what the color does, not what it is */
:root {
--color-background:        hsl(220, 14%, 96%);
--color-surface:           hsl(0, 0%, 100%);
--color-surface-raised:    hsl(220, 14%, 92%);
--color-text-primary:      hsl(220, 30%, 15%);
--color-text-secondary:    hsl(220, 15%, 40%);
--color-text-disabled:     hsl(220, 10%, 65%);
--color-border:            hsl(220, 13%, 84%);
--color-primary:           hsl(217, 91%, 60%);
--color-primary-hover:     hsl(221, 83%, 53%);
--color-primary-text:      hsl(0, 0%, 100%);
--color-success:           hsl(142, 71%, 35%);
--color-warning:           hsl(38, 92%, 45%);
--color-error:             hsl(0, 84%, 50%);
}

@media (prefers-color-scheme: dark) { :root { --color-background: hsl(220, 15%, 10%); --color-surface: hsl(220, 12%, 15%); --color-surface-raised: hsl(220, 10%, 20%); --color-text-primary: hsl(220, 10%, 93%); --color-text-secondary: hsl(220, 8%, 65%); --color-text-disabled: hsl(220, 6%, 40%); --color-border: hsl(220, 15%, 25%); --color-primary: hsl(217, 91%, 68%); /* lighter in dark mode */ --color-primary-hover: hsl(217, 91%, 75%); --color-primary-text: hsl(220, 15%, 10%); --color-success: hsl(142, 60%, 55%); --color-warning: hsl(38, 90%, 60%); --color-error: hsl(0, 80%, 65%); } }

/* Usage */ .btn-primary { background: var(--color-primary); color: var(--color-primary-text); border: 1px solid var(--color-primary); } .btn-primary:hover { background: var(--color-primary-hover); }

Modern CSS Color Functions

CSS Color Level 4 and 5 introduce functions that let you work with colors programmatically in CSS — no JavaScript required.

color-mix()

<code">/* Mix two colors in a given color space */
color: color-mix(in oklch, blue 70%, white);
color: color-mix(in srgb, #3b82f6 50%, transparent);

/* Useful for hover states */ .btn:hover { background: color-mix(in srgb, var(--color-primary) 85%, black); }

Relative Colors (CSS Color Level 5)

<code">/* Generate variants from a base color */
:root { --brand: hsl(217, 91%, 60%); }

.lighter { color: hsl(from var(--brand) h s calc(l + 15%)); } .darker { color: hsl(from var(--brand) h s calc(l - 15%)); } .muted { color: hsl(from var(--brand) h calc(s - 30%) l); }

/* Shift hue for a complementary color */ .complement { color: hsl(from var(--brand) calc(h + 180) s l); }

oklch and oklab — Perceptually Uniform Color

HSL's lightness value is not perceptually uniform — a yellow at L:50% looks much lighter than a blue at L:50%. The oklch and oklab color spaces are perceptually uniform, meaning equal changes in values produce equal changes in perceived lightness.

<code">/* oklch(lightness chroma hue) /
color: oklch(0.6 0.2 250);   / A medium blue */

/* Generating a palette with consistent perceived lightness / --step-1: oklch(0.95 0.02 250); --step-2: oklch(0.85 0.06 250); --step-3: oklch(0.70 0.12 250); --step-4: oklch(0.55 0.18 250); / base */ --step-5: oklch(0.40 0.18 250); --step-6: oklch(0.25 0.14 250);

Browser support for oklch is now excellent (96%+ of browsers as of 2026). It's worth using for new projects, especially when you need consistent perceptual lightness across different hues.

Tools

HEX to RGB Converter

Convert between HEX, RGB, HSL, and CMYK in one place.

Convert Colors
Contrast Checker

Check foreground/background combinations against WCAG AA and AAA requirements.

Check Contrast
Color Picker

Visual color picker with complementary, triadic, and analogous scheme generation.

Open Color Picker
CMYK to HEX

Convert print CMYK values to web-ready HEX and RGB.

Convert CMYK

Last updated: March 2026

All color conversions on ToolsDock happen in your browser using standard color math. No color data is sent to any server.

Privacy Notice: This site works entirely in your browser. We don't collect or store your data. Optional analytics help us improve the site. You can deny without affecting functionality.