# Color Science — A Practical Reference for Web Design

> **AI-compiled reference, not personally authored.** Verify critical claims — especially accessibility thresholds and contrast ratios — against canonical sources (W3C WCAG, MDN, caniuse) before shipping to production. Citations are inline; follow them.

This is a cheat sheet for picking colors for the HTML design samples in this workspace (see `CATALOG.md`). It is opinionated. When I say "I recommend," I mean it.

---

## 1. Color spaces on the web

### sRGB vs Display P3

Every `#hex`, `rgb()`, and `hsl()` value you have ever written lives inside **sRGB**, standardized in 1996 and covering roughly 35.9% of human-visible color.[^srgb] **Display P3** is Apple's wide-gamut profile — about 25% larger in surface area than sRGB, 50% larger in volume — and is now well supported across Safari (since 10), Chrome/Edge (111+), and Firefox (113+).[^p3]

```css
/* sRGB — safe everywhere */
background: #e8756a;

/* Display P3 — richer on capable displays, silently ignored elsewhere if you provide a fallback */
background: #e8756a;
background: color(display-p3 0.91 0.46 0.42);

@media (color-gamut: p3) {
  /* progressively enhance */
}
```

**Practical take:** P3 buys you meaningfully more vivid *saturated* colors (hot pinks, neon greens, electric blues). For muted/pastel palettes it is nearly invisible. Use sRGB as the baseline, layer P3 as enhancement only if the saturation actually calls for it.

### HSL vs OKLCH — and why `L` matters

HSL is RGB wearing a hat. Its `L` channel is not perceptual lightness — it is a mathematical transform of RGB. That is why **HSL 50% lightness** in yellow looks blinding while the same HSL `L` in blue looks midnight-dark.[^oklch-evil] **OKLCH** (cylindrical form of Oklab) solves this: `L` is perceptually uniform, so 60% lightness looks roughly the *same brightness* across every hue.[^oklch-mdn]

```css
/* All three have hsl() L = 50%. Perceived brightness varies wildly. */
background: hsl( 60 100% 50%); /* yellow — BRIGHT */
background: hsl(240 100% 50%); /* blue   — DARK   */

/* OKLCH — all three feel like "the same lightness" */
background: oklch(0.7 0.2  60); /* peach  */
background: oklch(0.7 0.2 180); /* teal   */
background: oklch(0.7 0.2 260); /* blue   */
```

**Why this matters for design systems:** when you generate `:hover` (+8% L), `:active` (-8% L), `disabled` (-chroma), or a 50/100/…/900 tint ramp, OKLCH gives you *predictable* steps. HSL gives you chaos — the hover on your blue button feels dead, while the hover on your yellow CTA blows out.[^colorbox]

**Browser support for `oklch()` / `oklab()`:** Chrome 111+, Firefox 113+, Safari 15.4+, Edge 111+ — all modern browsers.[^caniuse] You can ship it today with a hex fallback:

```css
color: #3b82f6;           /* sRGB fallback, old browsers */
color: oklch(0.62 0.19 252); /* modern browsers override */
```

---

## 2. Palette-building heuristics

### The 60/30/10 rule

Split your palette into **60% dominant** (backgrounds, large surfaces), **30% secondary** (supporting blocks, cards, nav), **10% accent** (CTAs, highlights, focus rings).[^603010] It is a starting pose, not a law — editorial layouts often run 80/15/5, and brutalist or maximalist designs cheerfully ignore it. But if you are lost, reach for it first.

### Harmonies — strengths and failure modes

| Scheme | What it is | Works for | Breaks when… |
|---|---|---|---|
| **Monochromatic** | One hue, varied `L`/`C` | Editorial, minimalist, data viz | You need visual hierarchy beyond lightness |
| **Analogous** | 3 adjacent hues (~30° apart) | Warm/cozy, nature, dashboards | No contrast — everything blurs |
| **Complementary** | Two opposite hues (180°) | Brand marks, sports, energy | Full-sat pairs *vibrate* (see below) |
| **Split-complementary** | Base + two neighbors of its complement | Most illustrated UI — best "safe-but-interesting" | Rarely |
| **Triadic** | 3 hues 120° apart | Playful, children's media | Everything screams; hard to balance |
| **Tetradic / square** | 4 hues in a rectangle | Rich editorial, seasonal | Requires one dominant; amateurs balance all four |

### Why complementaries vibrate — and how to fix it

Place `hsl(0 100% 50%)` next to `hsl(180 100% 50%)` on a shared edge. Your retina's cone cells are overstimulated at the boundary: the edge appears to shimmer. This is **simultaneous contrast**, a physiological effect, not a bug you can design around by wishing.[^vibrate]

Three fixes, in order of how often I reach for them:

1. **Desaturate one side.** Keep the accent punchy; soften the other to a muted, dusty neighbor. A burnt-orange against a chalky steel-blue reads as "warm and considered" instead of "alarm."
2. **Shift to split-complementary.** Instead of true opposite, pick the two colors flanking the opposite (~150° and ~210°). Same energy, no vibration.[^splitcomp]
3. **Bridge with a neutral.** Insert an off-white, cream, or tinted-gray surface between the two colors. The edge goes away because there is no edge.

### Build tinted neutrals from the accent, not pure gray

`#888` is dead. Real designers mix a trace of their accent hue into their grays, so the neutrals feel like they belong to the family. In OKLCH this is effortless:

```css
--accent: oklch(0.68 0.17 20);      /* warm pink */
--ink:    oklch(0.22 0.03 20);      /* near-black, warm bias */
--muted:  oklch(0.55 0.02 20);      /* mid gray, same hue */
--bg:     oklch(0.98 0.01 80);      /* warm off-white (shifted to cream) */
```

Warm palettes want warm neutrals (pink/yellow undertones); cool palettes want cool neutrals. Mixing a warm accent with a cool gray is the #1 reason a design feels "off" without users being able to say why.[^warmgray]

---

## 3. Accessibility / contrast

### WCAG 2.2 ratios — learn these by heart

| Content | Ratio (AA) | Ratio (AAA) |
|---|---|---|
| Body text (<18pt / <14pt bold) | **4.5:1** | 7:1 |
| Large text (≥18pt / ≥14pt bold) | **3:1** | 4.5:1 |
| Non-text UI (icons, input borders, focus rings) | **3:1** | — |

These are the operative legal standards in 2026.[^wcag22] WCAG 2.2 is currently the binding benchmark for ADA, EN 301 549, and most international regulations.

### APCA / WCAG 3 — the future, not the present

**APCA** (Advanced Perceptual Contrast Algorithm) is a proposed replacement that accounts for font weight, size, and the directionality of dark-on-light vs light-on-dark — things WCAG 2 completely ignores. APCA produces an `Lc` value (roughly -108 to +106) rather than a ratio.

Status as of April 2026: **WCAG 3 is still a Working Draft**, with Recommendation status not expected before 2028–2030. APCA is *not* in the current WCAG 3 draft — it has been proposed but the final algorithm is undecided.[^apca2026] [^apca-yatil]

**Practical rule:** Pass WCAG 2.2 AA for compliance. Use APCA *in addition* during design reviews to catch things WCAG 2 misses (e.g., thin light-gray text on white that "passes" 4.5:1 but is still miserable). Do not use APCA as your sole standard — yet.

### Color vision deficiency

About 8% of men and 0.5% of women have some form of CVD. The common types:[^cvd]

- **Deuteranopia / deuteranomaly** (green-weak) — ~6% of men. Most common. Red/green confusion.
- **Protanopia / protanomaly** (red-weak) — ~2% of men. Reds appear dark, red/green confusion.
- **Tritanopia** (blue-yellow) — <0.01%, rare. Affects both sexes equally.

**The only rule you need:** **never encode information with hue alone.** Add an icon, a pattern, a label, an underline. A red "error" and a green "success" are indistinguishable to ~1 in 12 of your male users. Red underline + check icon vs green underline + X icon — problem solved.

---

## 4. Pitfalls and anti-patterns

- **Pure `#000` on `#fff` body text.** The halation effect: at maximum contrast, white bleeds into the black, your pupils contract, and after five minutes you have a headache.[^darkmode] Use `#111–#1a1a1a` text on `#fafafa–#fdfdf9` backgrounds. You keep the contrast, lose the strain.
- **Fully-saturated pure blue (`#0000ff`) as body text.** Worst offender for chromatic aberration: the eye's lens refracts short wavelengths (blue) differently from long (red), and your fovea literally cannot focus both at once. Pure-blue links on white are tiring; pure blue *body copy* is unreadable.[^blue] Use a desaturated blue (`oklch(0.45 0.12 250)`) or shift toward teal.
- **AI slop rainbow gradients.** Three accents is rich. Five is loud. Seven is a fruit salad. If every element is "special," nothing is. Pick one hero accent and support it with neutrals + one quiet secondary.
- **Gradients with a muddy midpoint (the default sRGB interpolation trap).** A `linear-gradient(135deg, #E89B89, #3FBFB5)` looks clean at the endpoints and *taupe-grey* in the middle. That is because CSS interpolates each RGB channel independently through sRGB, which averages opposing hues toward a dirty grey. CSS Color Module 4 lets you pick the interpolation color space — but the right choice depends on what you want the *middle* to look like:[^gradient-interp]

    ```css
    /* sRGB default — muddy olive-taupe through the middle */
    background: linear-gradient(135deg, #E89B89, #3FBFB5);

    /* in oklab — clean NEUTRAL midpoint at matched perceptual lightness.
       The usual right answer for "pink to blue without anything ugly in between." */
    background: linear-gradient(135deg in oklab, #E89B89, #3FBFB5);

    /* in oklch — SHORT-arc hue interpolation. Between pink (~35°) and teal (~188°)
       this passes through yellow and green. Great if you want that journey;
       surprising if you don't. */
    background: linear-gradient(135deg in oklch, #E89B89, #3FBFB5);

    /* in oklch longer hue — LONG-arc. Same two endpoints, but travels the
       other way around the wheel: through reds, magentas, and purples.
       Stays on the warm-cool axis visually. */
    background: linear-gradient(135deg in oklch longer hue, #E89B89, #3FBFB5);

    /* Hand-placed midpoint — designer picks the middle color explicitly.
       Maximum control; no spec surprises. */
    background: linear-gradient(135deg, #E89B89, #D8B5AE 50%, #3FBFB5);
    ```

    **The distinction:** **Oklab** is Cartesian (L, a, b) — interpolating between opposing `a/b` values lands on `(0,0)` at the midpoint, a clean neutral at the matched lightness. **OKLCH** is cylindrical (L, C, h) — it holds chroma constant and rotates hue, which means a short arc between two far-apart hues passes through whatever happens to sit on the arc (yellow/green for pink→teal short; red/magenta/purple for long). If you want a plain pink-to-blue with a clean middle, reach for `in oklab`. If you want chromatic travel through specific intermediate hues, reach for `in oklch` and decide short vs long deliberately.

    `in oklch longer hue` is also how you get rainbow-arc gradients without manually placing stops. Support for `in <color-space>`: Chrome 111+, Safari 16.2+, Firefox 113+.[^caniuse-interp] Provide a plain-hex gradient as a fallback if you care about older browsers. The underlying pattern is the same one that broke HSL generated ramps (§1) — interpolate in the space that matches perception, not the space the hardware reads.
- **Dark mode by `filter: invert(1)`.** No. Dark mode needs (a) desaturated versions of your brand colors (saturation reduced ~10–20%), (b) *warmer* grays than your light mode (cool grays go dead-blue in low light), (c) no pure `#fff` text — use `#e0e0e0–#f0f0f0`.[^darkmode] Material recommends `#121212` as the base surface, not `#000`.

---

## 5. Applied analysis — Himalayan rock salt pink + tropical sea blue

### What these colors actually are

**Himalayan rock salt** ranges from pale pink through salmon to deep reddish-maroon depending on iron-oxide concentration.[^salt] The *good* reference is warm, mineral, slightly dusty — a salmon-coral-peach with beige undertones. It is **not** bubblegum, not millennial pink, not cotton candy. Commonly cited anchors cluster around `#d1bab5` (Pantone "Himalayan Salt") through `#e8a598` to `#efbfb0`.

**Tropical sea blue** (Caribbean / Maldivian shallows) is a **bright cyan-turquoise**, not navy, not cobalt.[^caribbean] Typical hex anchors: `#00a6a0`, `#2dd4bf`, `#6cdae7`, `#7ee6de`. Undertones lean slightly green (teal side) in shallow water, slightly blue in deeper water. The *resort brochure* version is around `#4fd1c5`.

### The pairing

Salt-pink (~20° OKLCH hue) and tropical-cyan (~195° OKLCH hue) sit roughly 175° apart — nearly complementary, technically **split-complementary-leaning**. At full saturation they will absolutely vibrate. The moves to tame it, which the recommended anchors below apply:

1. Pull the pink slightly toward **peach/coral** (away from magenta; hue ~20–30 in OKLCH, not ~0).
2. Pull the blue slightly toward **teal** (hue ~190–200, not ~220).
3. Anchor on a **warm off-white** background (shifted toward cream, not pure white) and a **deep warm-tinted charcoal** for ink.
4. Keep both accents at moderate chroma (~0.10–0.14), never at max.

### Recommended 6-color palette

| Role | Hex | OKLCH | HSL | Notes |
|---|---|---|---|---|
| `--bg` warm off-white | `#FAF4EE` | `oklch(0.96 0.014 75)` | `hsl(33 55% 96%)` | Cream, not white — reduces halation |
| `--ink` deep tinted charcoal | `#2A2320` | `oklch(0.24 0.012 40)` | `hsl(18 14% 14%)` | Warm-bias black, not `#000` |
| `--muted` warm mid-gray | `#9A8A84` | `oklch(0.62 0.016 35)` | `hsl(15 9% 56%)` | Derived from salt-pink hue at low chroma |
| `--pink` salt-pink primary | `#E89B89` | `oklch(0.74 0.11 35)` | `hsl(12 69% 73%)` | Warm coral-peach, the "salt mineral" |
| `--pink-deep` | `#C96F5C` | `oklch(0.62 0.14 32)` | `hsl(11 53% 57%)` | For text/pressed states on pink bg |
| `--blue` tropical turquoise | `#3FBFB5` | `oklch(0.72 0.11 188)` | `hsl(176 51% 50%)` | Caribbean shallows |
| `--blue-deep` | `#1F7C78` | `oklch(0.51 0.09 190)` | `hsl(177 60% 30%)` | For ink-on-light-blue, hover states |

### WCAG-tested combinations (AA)

| Foreground | Background | Ratio | Use |
|---|---|---|---|
| `--ink` `#2A2320` | `--bg` `#FAF4EE` | **~13.5:1** | Body text — AAA |
| `--pink-deep` `#C96F5C` | `--bg` `#FAF4EE` | ~3.4:1 | Large text / headings only |
| `--blue-deep` `#1F7C78` | `--bg` `#FAF4EE` | ~4.8:1 | Body text — AA |
| `--bg` `#FAF4EE` | `--blue-deep` `#1F7C78` | ~4.8:1 | White text on teal button |
| `--ink` `#2A2320` | `--pink` `#E89B89` | ~7.2:1 | Ink on pink card — AAA |
| `--ink` `#2A2320` | `--blue` `#3FBFB5` | ~6.5:1 | Ink on teal card — AA+ |

*(Verify with a real contrast checker before shipping — these are computed approximations.)*

### Two expression modes

**(a) Salt mineral / spa — pink-dominant**
Surfaces 70% `--bg` + `--pink` tints. Teal used sparingly on CTAs, icons, rule lines. Typography in `--ink`. Feels quiet, wellness-adjacent, editorial. Pairs with serif display + humanist sans body.

**(b) Tropical lagoon / resort — blue-dominant**
Large teal panels (`--blue`) with cream type, pink as a single accent (call-to-action, brand mark, photo tint). Feels breezy, hospitality, travel. Pairs with rounded geometric sans and generous white space.

### Failure modes to avoid

- **Saccharine "cotton candy beach."** Happens if both pink and blue slide toward pastel (`L > 0.85`) and the background is pure white. Fix: deepen the ink, warm the background, keep the accents at `L ~0.70` not `L ~0.88`.
- **Medical / pharmaceutical.** Happens if pink drifts toward bubblegum `#ffc0cb` and teal drifts toward clinical cyan `#00bcd4` on stark white. Fix: ground everything on warm cream, push pink toward coral (add red + yellow, lose the magenta), push teal toward sea green.
- **Tropical clip-art.** Happens if you let both colors run at max chroma on large surfaces. Fix: one accent is always the star; the other is a garnish. Obey 60/30/10.

---

## 6. Tools and resources

- **[Coolors](https://coolors.co)** — fastest palette generator; spacebar to iterate, lock swatches you like.
- **[Leonardo (Adobe)](https://leonardocolor.io)** — open-source, contrast-ratio-driven palette generation. Best for design-system token ramps.[^leonardo]
- **[oklch.com](https://oklch.com)** — visual OKLCH picker with gamut clipping warnings.
- **[HueLRV](https://huelrv.com)** — visualizes perceived lightness across hues; great for sanity-checking HSL palettes.
- **[Who Can Use](https://whocanuse.com)** — simulates how a color pair is seen across CVD types and low-vision conditions.
- **[Stark](https://www.getstark.co)** — Figma/Sketch plugin, best-in-class accessibility auditing including APCA.
- **[accessible-palette.com](https://accessible-palette.com)** — generates AA/AAA-compliant palettes from seed colors using LCH.
- **[WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)** — canonical WCAG 2 checker.

---

### Footnotes

[^srgb]: [sRGB vs Display P3 — ColorFYI](https://colorfyi.com/blog/srgb-vs-display-p3/); [Improving Color on the Web — WebKit](https://webkit.org/blog/6682/improving-color-on-the-web/)
[^p3]: [Wide Gamut Color in CSS with Display-P3 — WebKit](https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/); [High-definition CSS color guide — Chrome Developers](https://developer.chrome.com/docs/css-ui/high-definition-css-color-guide)
[^oklch-evil]: [OKLCH in CSS: why we moved from RGB and HSL — Evil Martians](https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl)
[^oklch-mdn]: [oklch() — MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/oklch); [Oklab color space — Wikipedia](https://en.wikipedia.org/wiki/Oklab_color_space)
[^colorbox]: [OKLCH vs HSL: Why Perceptual Uniformity Matters — ColorBox](https://colorbox.io/oklch-vs-hsl)
[^caniuse]: [Can I use — oklch()](https://caniuse.com/mdn-css_types_color_oklch)
[^603010]: [60-30-10 Color Rule — FlowMapp](https://www.flowmapp.com/blog/qa/60-30-10-rule); [60-30-10 in UI Design — HYPE4.Academy](https://hype4.academy/articles/design/60-30-10-rule-in-ui)
[^vibrate]: [A Matter of Contrasts — Nightingale / Theresa-Marie Rhyne](https://medium.com/nightingale/a-matter-of-contrasts-7288d1dae05e); [Complementary Colors — Atmos Style](https://atmos.style/glossary/complementary-colors)
[^splitcomp]: [Split-Complementary Colors — Figma resource library](https://www.figma.com/resource-library/what-are-split-complementary-colors/)
[^warmgray]: [Color in Design Systems — Nathan Curtis / EightShapes](https://medium.com/eightshapes-llc/color-in-design-systems-a1c80f65fa3)
[^wcag22]: [Understanding SC 1.4.3: Contrast (Minimum) — W3C WAI](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html); [WCAG 2.2 — W3C](https://www.w3.org/TR/WCAG22/)
[^apca2026]: [WCAG3 Contrast as of April 2026 — Adrian Roselli](https://adrianroselli.com/2026/04/wcag3-contrast-as-of-april-2026.html)
[^apca-yatil]: [WCAG 3 is not ready yet — Eric Eggert](https://yatil.net/blog/wcag-3-is-not-ready-yet)
[^cvd]: [Color blindness — Wikipedia](https://en.wikipedia.org/wiki/Color_blindness); [Types of Color Vision Deficiency — National Eye Institute](https://www.nei.nih.gov/eye-health-information/eye-conditions-and-diseases/color-blindness/types-color-vision-deficiency)
[^darkmode]: [Dark Mode UI Best Practices — Atmos](https://atmos.style/blog/dark-mode-ui-best-practices); [Why You Should Never Use Pure Black for Text — UX Movement](https://uxmovement.com/content/why-you-should-never-use-pure-black-for-text-or-backgrounds/)
[^blue]: [Applying Color Theory to Digital Displays — UXmatters](https://www.uxmatters.com/mt/archives/2007/01/applying-color-theory-to-digital-displays.php); [MIT 6.813 Reading 16: Color](https://web.mit.edu/6.813/www/sp16/classes/16-color/)
[^salt]: [Himalayan salt — Wikipedia](https://en.wikipedia.org/wiki/Himalayan_salt); [Pantone PMS 20-0090 TPM "Himalayan Salt" — Encycolorpedia](https://encycolorpedia.com/d1bab5)
[^caribbean]: [Caribbean Turquoise — iColorPalette](https://icolorpalette.com/color/caribbean-turquoise); [Tropical Waters scheme — SchemeColor](https://www.schemecolor.com/tropical-waters.php)
[^leonardo]: [Leonardo — leonardocolor.io](https://leonardocolor.io/); [Adobe Leonardo open source — InfoQ](https://www.infoq.com/news/2020/03/adobe-leonardo-accessible-colors/)
[^gradient-interp]: [CSS Color Module 4 — Color Interpolation](https://www.w3.org/TR/css-color-4/#interpolation); [CSS Images Module 4 — Gradient interpolation color space](https://drafts.csswg.org/css-images-4/#color-interpolation); [linear-gradient() — MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient) (see "color interpolation method" examples)
[^caniuse-interp]: [Can I use — `gradient color space` / color interpolation method](https://caniuse.com/mdn-css_types_gradient_linear-gradient_interpolation_color_space)
