Color Theory for Screens: RGB, HSL, Contrast, and Dark Mode
How digital color works from the physics of light to contrast ratios, WCAG accessibility, and why dark mode is more than just inverting colors.
Terminology
| Term | Definition |
|---|---|
| RGB | Additive color model that combines Red, Green, and Blue light channels, each ranging from 0 to 255, to produce colors on screens |
| HSL | Color model using Hue (0-360 degrees on a color wheel), Saturation (0-100%), and Lightness (0-100%) for more intuitive color manipulation |
| Color space | A defined range (gamut) of colors that a model can represent. sRGB, Display P3, and Rec. 2020 are common screen color spaces with progressively wider gamuts |
| Gamut | The complete subset of colors a device or color space can reproduce. Colors outside the gamut are "clipped" to the nearest representable value |
| Relative luminance | A measure of how bright a color appears to the human eye, weighted by our sensitivity to green, red, and blue light. Defined on a 0 (black) to 1 (white) scale |
| Contrast ratio | The ratio of relative luminance between a foreground and background color, expressed as $L_1 : L_2$ where $L_1$ is the lighter. WCAG requires at least 4.5:1 for normal text |
| WCAG | Web Content Accessibility Guidelines, a set of standards for making web content perceivable, operable, and understandable for people with disabilities |
| Gamma correction | A nonlinear transfer function applied to pixel values so that brightness steps appear perceptually uniform to the human eye. sRGB uses a gamma of approximately 2.2 |
| Additive color mixing | Combining light sources where overlapping beams add energy, producing brighter results. Red + Green + Blue at full intensity yields white |
| Dark mode | A UI color scheme using light text on dark backgrounds. Requires careful re-mapping of the entire color palette, not a simple luminance inversion |
What & Why
Every pixel on your screen is a tiny cluster of three sub-pixels: one red, one green, one blue. By varying the intensity of each, the display mixes light to produce millions of colors. This is additive color mixing, and it is the physical foundation of all digital color.
Understanding color theory for screens matters for three practical reasons. First, choosing colors in the wrong model leads to muddy palettes and unpredictable results. Second, ignoring contrast ratios makes text unreadable for millions of users with low vision. Third, building dark mode by naively inverting colors produces glaring whites, washed-out brand colors, and broken visual hierarchies.
This post walks through the core color models (RGB, HSL), the concept of color spaces and gamuts, the math behind contrast ratios and WCAG compliance, and the design principles that make dark mode actually work.
How It Works
Additive Color Mixing and RGB
Screens emit light rather than reflect it, so they use additive mixing. Each pixel has three channels, Red (R), Green (G), and Blue (B), each with an integer intensity from 0 to 255 (8 bits per channel). This gives $256^3 = 16{,}777{,}216$ possible colors.
A hex color like #4F46E5 encodes R=79, G=70, B=229. The notation is compact but not intuitive: looking at those three numbers, it is hard to tell that this is a vivid indigo. That is where HSL comes in.
HSL: A Human-Friendly Model
HSL re-maps the same colors into three axes that match how humans think about color:
- Hue: the "pure color" as an angle on a color wheel (0 = red, 120 = green, 240 = blue)
- Saturation: how vivid the color is (0% = gray, 100% = fully saturated)
- Lightness: how bright or dark (0% = black, 50% = pure color, 100% = white)
HSL makes palette design practical. To create a lighter variant of a brand color, increase lightness. To create a muted version, decrease saturation. To create a complementary color, rotate hue by 180 degrees. These operations are awkward in RGB but trivial in HSL.
Color Spaces and Gamuts
A color model (like RGB) defines how to encode a color. A color space defines which specific colors those numbers map to. The same RGB triplet (200, 50, 50) means different actual colors in different color spaces because each space defines different "anchor" primaries.
The key color spaces for screen work:
- sRGB: the default web color space. When you write
color: #4F46E5in CSS, the browser assumes sRGB. Covers about 35% of visible colors. - Display P3: roughly 25% wider gamut than sRGB. Supported by modern Apple devices and many newer monitors. CSS supports it via
color(display-p3 0.31 0.27 0.90). - Rec. 2020: the target for HDR video. Covers about 75% of visible colors. Most current displays cannot reproduce its full range.
For web development, sRGB is the safe default. Use Display P3 as a progressive enhancement with @media (color-gamut: p3) queries.
Relative Luminance and Contrast Ratios
Not all colors appear equally bright. Human eyes are most sensitive to green light, moderately sensitive to red, and least sensitive to blue. The WCAG formula for relative luminance captures this:
where $R_{\text{lin}}$, $G_{\text{lin}}$, $B_{\text{lin}}$ are the linearized (gamma-decoded) channel values. To linearize an sRGB value $C_{\text{srgb}}$ (normalized to 0-1):
The contrast ratio between two colors is then:
The 0.05 offset accounts for ambient light reflected off the screen. The result ranges from 1:1 (identical colors) to 21:1 (black on white).
WCAG Contrast Requirements
WCAG 2.1 defines two conformance levels for text contrast:
| Level | Normal text | Large text (18pt+ or 14pt bold) | UI components |
|---|---|---|---|
| AA (minimum) | 4.5 : 1 | 3 : 1 | 3 : 1 |
| AAA (enhanced) | 7 : 1 | 4.5 : 1 | not defined |
Why Dark Mode Is Not Just Inverting Colors
A naive approach to dark mode is to invert lightness: swap white backgrounds to black and black text to white. This fails for several reasons:
-
Pure black (#000) backgrounds cause halation. Light text on pure black creates a high-contrast glare effect where letterforms appear to bleed outward, especially for users with astigmatism. Dark mode backgrounds should use dark grays like #121212 or #1a1a2e.
-
Inverted brand colors lose their identity. A brand blue at
hsl(240, 80%, 50%)inverted tohsl(240, 80%, 50%)stays the same (lightness 50% is the midpoint), but a brand orange athsl(30, 90%, 60%)inverted tohsl(30, 90%, 40%)becomes a muddy brown. -
Elevation and depth reverse. In light mode, shadows create depth. In dark mode, lighter surfaces indicate elevation. Material Design uses surface overlays: each elevation level adds a semi-transparent white overlay to the base dark surface.
-
Contrast ratios shift. A color pair that passes WCAG AA on a white background may fail on a dark background because the luminance relationship changes.
The correct approach to dark mode is to build a separate color palette where each semantic token (background, surface, text-primary, text-secondary, accent) is intentionally mapped to dark-appropriate values. This preserves visual hierarchy, maintains contrast ratios, and keeps brand colors recognizable.
Complexity Analysis
Color computations are per-pixel or per-color-pair operations. The key costs:
| Operation | Time | Space | Notes |
|---|---|---|---|
| RGB to HSL conversion | $O(1)$ | $O(1)$ | Fixed arithmetic on 3 channels |
| Relative luminance | $O(1)$ | $O(1)$ | 3 gamma decodes + weighted sum |
| Contrast ratio (1 pair) | $O(1)$ | $O(1)$ | 2 luminance calculations + division |
| Validate all $n$ color pairs in a palette | $O(n)$ | $O(n)$ | Linear scan of palette entries |
| All-pairs contrast check ($n$ colors) | $O(n^2)$ | $O(n^2)$ | Every foreground/background combination |
| Gamut mapping (clamp to sRGB) | $O(1)$ | $O(1)$ | Clamp each channel independently |
For a design system with $n$ semantic color tokens, validating that every possible foreground/background pairing meets WCAG AA requires checking $\binom{n}{2}$ pairs:
For a typical palette of 20 tokens, that is $\frac{20 \times 19}{2} = 190$ contrast checks, each $O(1)$, so the total validation is fast in practice.
Gamma Decode Cost
The sRGB linearization involves a conditional branch and a power function ($x^{2.4}$). For real-time rendering of millions of pixels, GPUs use hardware lookup tables or fast polynomial approximations to avoid the per-pixel exponentiation:
This simplified gamma approximation trades a small accuracy loss for a single power operation instead of the piecewise sRGB transfer function.
Implementation
Computing Relative Luminance
FUNCTION relativeLuminance(r, g, b)
INPUT: r, g, b as integers in [0, 255]
OUTPUT: luminance as a float in [0, 1]
rNorm ← r / 255
gNorm ← g / 255
bNorm ← b / 255
rLin ← linearize(rNorm)
gLin ← linearize(gNorm)
bLin ← linearize(bNorm)
RETURN 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin
END FUNCTION
FUNCTION linearize(c)
INPUT: c as a float in [0, 1] (sRGB normalized)
OUTPUT: linearized value
IF c <= 0.04045 THEN
RETURN c / 12.92
ELSE
RETURN ((c + 0.055) / 1.055) ^ 2.4
END IF
END FUNCTION
Computing Contrast Ratio
FUNCTION contrastRatio(fg, bg)
INPUT: fg, bg as (r, g, b) tuples
OUTPUT: contrast ratio as a float >= 1
lumFg ← relativeLuminance(fg.r, fg.g, fg.b)
lumBg ← relativeLuminance(bg.r, bg.g, bg.b)
lighter ← MAX(lumFg, lumBg)
darker ← MIN(lumFg, lumBg)
RETURN (lighter + 0.05) / (darker + 0.05)
END FUNCTION
WCAG Compliance Check
FUNCTION meetsWCAG(fg, bg, level, isLargeText)
INPUT: fg, bg as (r, g, b); level as "AA" or "AAA"; isLargeText as boolean
OUTPUT: true if the pair meets the required contrast
cr ← contrastRatio(fg, bg)
IF level = "AA" THEN
IF isLargeText THEN
RETURN cr >= 3.0
ELSE
RETURN cr >= 4.5
END IF
ELSE IF level = "AAA" THEN
IF isLargeText THEN
RETURN cr >= 4.5
ELSE
RETURN cr >= 7.0
END IF
END IF
END FUNCTION
RGB to HSL Conversion
FUNCTION rgbToHsl(r, g, b)
INPUT: r, g, b as integers in [0, 255]
OUTPUT: (h, s, l) where h in [0, 360), s and l in [0, 1]
rNorm ← r / 255
gNorm ← g / 255
bNorm ← b / 255
cMax ← MAX(rNorm, gNorm, bNorm)
cMin ← MIN(rNorm, gNorm, bNorm)
delta ← cMax - cMin
l ← (cMax + cMin) / 2
IF delta = 0 THEN
h ← 0
s ← 0
ELSE
IF l < 0.5 THEN
s ← delta / (cMax + cMin)
ELSE
s ← delta / (2 - cMax - cMin)
END IF
IF cMax = rNorm THEN
h ← 60 * (((gNorm - bNorm) / delta) MOD 6)
ELSE IF cMax = gNorm THEN
h ← 60 * (((bNorm - rNorm) / delta) + 2)
ELSE
h ← 60 * (((rNorm - gNorm) / delta) + 4)
END IF
IF h < 0 THEN
h ← h + 360
END IF
END IF
RETURN (h, s, l)
END FUNCTION
Real-World Applications
- Design systems: Figma, Material Design, and Tailwind CSS all define color palettes using HSL-based scales (e.g., Tailwind's blue-50 through blue-950) so that lightness and saturation vary predictably across the scale
- Accessibility auditing: Tools like axe-core, Lighthouse, and Stark run the WCAG contrast ratio formula against every text/background pair in a rendered page to flag violations automatically
- Dark mode in production: macOS, iOS, Android, and Windows all provide system-level dark mode APIs. Apps that respect these APIs re-map semantic color tokens rather than inverting pixel values
- Wide-gamut photography: Photo editing apps like Lightroom work in ProPhoto RGB (very wide gamut) internally, then convert to sRGB or Display P3 for export, clipping out-of-gamut colors gracefully
- Video streaming: HDR content on Netflix and YouTube uses Rec. 2020 color space with PQ (Perceptual Quantizer) transfer function, requiring tone-mapping for SDR displays
- Game engines: Unity and Unreal perform lighting calculations in linear color space, then apply gamma correction as a final post-processing step before display
Key Takeaways
- RGB is how screens physically produce color (additive mixing of red, green, blue light), but HSL is far more practical for designing palettes because hue, saturation, and lightness map to human intuition
- A color space defines which real-world colors the numbers represent. sRGB is the web default, Display P3 is the progressive enhancement for modern screens
- Contrast ratio is computed from relative luminance, which weights green heavily (71.52%) because human eyes are most sensitive to green light
- WCAG AA requires at least 4.5:1 contrast for normal text and 3:1 for large text. These are minimums, not targets
- Dark mode done right means building a separate semantic color palette, not inverting lightness. Use dark gray backgrounds (#121212), lighter surfaces for elevation, and reduced saturation to avoid glare
Read More
2025-07-16
Typography Fundamentals: Anatomy, Hierarchy, and Why Line-Height Matters
How typeface anatomy, font pairing, visual hierarchy, and variable fonts work, and why developers who understand line-height build better interfaces.
2025-07-15
HCI Foundations: The Laws That Govern Every Click
Fitts's law, Hick's law, Miller's 7 plus or minus 2, cognitive load theory, mental models, and affordances: the science behind usable interfaces.
2025-07-16
Usability Heuristics: Nielsen's 10 Rules for Interface Design
A deep dive into Nielsen's 10 usability heuristics, heuristic evaluation methodology, severity ratings, and common violations in real products.