Notes/Color Theory for Screens: RGB, HSL, Contrast, and Dark Mode
Back to Notes

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.

2025-07-15AI-Synthesized from Personal NotesSource2100+ words of raw notesEnrichmentsCode blocks, GraphicsPipelineMulti-pass AI review · Score: 98/100
Share
Design UxColor TheoryAccessibilityDesign

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.

R B G Magenta Yellow Cyan White Additive RGB: overlapping channels produce secondary colors and white

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)
60° 120° 180° 240° 300° Hue wheel sampled at 60° intervals (S=100%, L=50%) S=0% S=25% S=50% S=75% S=100% Saturation ramp for H=240° (blue) at L=50%

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.

CIE visible spectrum (simplified) sRGB Display P3 Rec. 2020 Wider triangles cover more visible colors (larger gamut)

The key color spaces for screen work:

  • sRGB: the default web color space. When you write color: #4F46E5 in 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:

$L = 0.2126 \cdot R_{\text{lin}} + 0.7152 \cdot G_{\text{lin}} + 0.0722 \cdot B_{\text{lin}}$

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):

$C_{\text{lin}} = \begin{cases} \frac{C_{\text{srgb}}}{12.92} & \text{if } C_{\text{srgb}} \leq 0.04045 \\ \left(\frac{C_{\text{srgb}} + 0.055}{1.055}\right)^{2.4} & \text{otherwise} \end{cases}$

The contrast ratio between two colors is then:

$CR = \frac{L_{\text{lighter}} + 0.05}{L_{\text{darker}} + 0.05}$

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:

  1. 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.

  2. Inverted brand colors lose their identity. A brand blue at hsl(240, 80%, 50%) inverted to hsl(240, 80%, 50%) stays the same (lightness 50% is the midpoint), but a brand orange at hsl(30, 90%, 60%) inverted to hsl(30, 90%, 40%) becomes a muddy brown.

  3. 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.

  4. 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.

Light Mode Card (shadow = depth) Elevated card Dark Mode Card (base surface) Elevated (lighter = higher)

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:

$\text{pairs} = \frac{n(n-1)}{2}$

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:

$C_{\text{lin}} \approx C_{\text{srgb}}^{2.2}$

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