Dustin Johnson ← Design Primitives
Design Primitive Reference · Part III-B · April 2026

Saturation & Chroma

How color intensity interacts with contrast, readability, and perceptual weight — and why the right chroma for a button depends on its hue, its role, and whether it's a fill, a text color, or a border.

Why Saturation Is Its Own Topic

Part III-A established the color budget, the hue map, and the hierarchy-semantic matrix. But every specimen in that document used colors at a single, "standard" saturation — the midpoint that felt natural for each hue. That was a simplification. In practice, saturation is a separate axis of design decisions with its own constraints, tradeoffs, and failure modes.

A fully saturated blue button and a muted blue button with the same lightness will produce the same WCAG contrast ratio with white text. Contrast, as measured by WCAG 2.x, is a pure function of relative luminance — it is entirely hue-blind and saturation-blind. And yet the two buttons do not read the same way. The saturated one feels louder, more urgent, more "primary." The muted one feels quieter, more restrained, more "background." This perceptual difference is real, meaningful, and not captured by any contrast formula.

This document maps the saturation dimension: from zero chroma (pure gray) through the muted and pastel ranges, through the "standard" range where most design systems operate, to the maximum chroma boundary where sRGB gamut clips. Along the way, it identifies the zones where buttons become too washed out to read as interactive, where colors vibrate against dark backgrounds, and where different hues have dramatically different available chroma ranges.

A1. Chroma vs. Saturation: The Distinction That Matters

HSL saturation is a lie. It reports 100% for both a vivid blue and a vivid yellow, despite the fact that the blue appears dramatically more intense to the human eye. OKLCH chroma is the corrective: a perceptually uniform measure of colorfulness that accurately reflects what you actually see.

In HSL, saturation is relative to the current hue and lightness — it simply measures how far the color is from the equivalent gray. This makes it useless for cross-hue comparison. A blue at hsl(220, 80%, 50%) and a yellow at hsl(50, 80%, 50%) have the same "saturation" number but wildly different perceptual intensity. The yellow appears far more vivid because yellow inherently carries more luminance.

OKLCH chroma (the C axis) solves this by measuring colorfulness on an absolute perceptual scale. A chroma of 0.15 looks roughly equally vivid regardless of hue. This makes it the correct tool for constructing button palettes where different semantic colors need to feel equally intense.

HSL S:80% across hues — supposedly equal saturation, visually unequal
Red
Orange
Yellow
Green
Cyan
Blue
Purple
Pink
OKLCH C:0.15, L:0.55 across hues — perceptually uniform intensity
Red
Orange
Yellow
Green
Cyan
Blue
Purple
Pink

Notice the difference. The HSL row has wild visual variation — the yellow and cyan feel like they're glowing while the blue and purple feel much heavier. The OKLCH row feels even. Each hue carries approximately the same visual weight. This is why OKLCH is the correct space for building a button color system: it lets you reason about chroma independently of hue, and trust that the numbers produce consistent visual outcomes.

For this document, "chroma" refers to the OKLCH C value. When we need to reference HSL saturation for context (since many designers still think in HSL), we will say so explicitly. All button specimens use OKLCH notation. The practical chroma range for sRGB is 0 (gray) to approximately 0.27–0.37 depending on hue and lightness, with most useful button fills landing between 0.08 and 0.22.

B1. The Chroma Spectrum for Button Fills

Not all chroma levels work for button backgrounds. Too low and the button looks disabled or decorative. Too high and it overwhelms the interface. The viable range is narrower than you might expect, and it varies by hue.

Here is a single hue (blue, H:260) at a fixed lightness suitable for white text (L:0.50), shown across the full chroma range from zero to maximum sRGB gamut. The question for each level: does this read as an interactive, clickable button?

Blue H:260 · L:0.50 · chroma from 0 to max sRGB · white text
0.00 Gray
0.03 Tinted
0.06 Muted
0.10 Soft
0.14 Standard
0.18 Vivid
0.22 Intense
0.27 Max sRGB

At chroma 0.00–0.03, there is no discernible color. The button reads as neutral gray. At 0.06, a faint blue tint is visible but the button feels "disabled" or "muted" — most users would not read it as a primary call-to-action. At 0.10–0.14, the color is clearly identifiable as blue and the button feels interactive. This is the lower threshold for a standard-weight button fill. At 0.18–0.22, the button feels confident and saturated — this is where most modern design systems land for their primary CTA. Above 0.22, the color becomes quite intense and can feel aggressive.

Now the same test for red (H:25):

Red H:25 · L:0.50 · chroma from 0 to max sRGB · white text
0.00 Gray
0.03 Tinted
0.06 Muted
0.10 Soft
0.14 Standard
0.18 Vivid
0.20 Intense
0.23 Max sRGB

The red strip behaves similarly but reaches its sRGB gamut limit sooner. This reveals the first key principle: every hue has a different maximum chroma at a given lightness. Blue can push to ~0.27 at L:0.50 within sRGB. Red maxes out around 0.23. Green maxes around 0.18. Yellow, because it requires much higher lightness to be recognizable, has entirely different limits.

The Practical Chroma Zones

Chroma rangeNameButton viability
0.00–0.03 Near-achromatic No color identity. Reads as neutral/gray. Usable only for neutral-role buttons.
0.03–0.07 Tinted gray Faint color cast. Feels disabled or decorative. Not viable for primary fills. Useful for tinted ghost fills and disabled states.
0.07–0.12 Muted / soft Color is identifiable but quiet. Suitable for secondary and tertiary buttons. Feels understated — editorial, luxury, or subdued brands.
0.12–0.20 Standard / vivid The sweet spot for primary button fills. Color is unambiguous, button feels interactive and confident. Most design systems operate here.
0.20–0.30 Intense / neon Very saturated. Can feel aggressive or "marketing." Use sparingly — single CTA on a landing page, not in a dense application.
0.30+ Out of sRGB gamut Requires P3 displays. Will be clipped on standard monitors. Not safe for production button systems.

C1. The Chroma-Contrast Independence

This is the most counterintuitive fact in button color design: changing saturation while holding lightness constant does not change the WCAG contrast ratio. A gray button and a vivid blue button at the same OKLCH lightness produce the same ratio against white text.

WCAG 2.x contrast is computed from relative luminance, which is a weighted sum of linearized R, G, and B values. When you add chroma (color intensity) to a surface at a fixed perceptual lightness, the R, G, and B channels redistribute but their weighted luminance sum stays approximately the same. The contrast ratio barely moves.

Same OKLCH lightness (L:0.50), increasing chroma · contrast vs. white text
C: 0.00 5.6:1
C: 0.08 5.5:1
C: 0.15 5.4:1
C: 0.22 5.3:1

The ratios barely change — from 5.6:1 to 5.3:1 across the full chroma range. All four buttons pass WCAG AA. From a compliance perspective, they are interchangeable. But they are obviously not perceptually interchangeable. The gray button and the vivid blue button feel completely different in weight, urgency, and interactivity.

This reveals the fundamental gap in WCAG's model as it applies to buttons: contrast ratio measures legibility, not communicative power. A muted, low-chroma button can pass contrast ratios while failing to signal "this is a primary action" because it lacks the visual assertiveness that chroma provides. Conversely, a highly saturated button can pass contrast while feeling so aggressive that users hesitate to click it.

WCAG is a floor, not a design guide. Passing 4.5:1 means text is legible. It does not mean the button communicates its role effectively. The chroma dimension is where design judgment — not algorithmic measurement — determines success. Your primary CTA needs enough chroma to feel like the most important element, while your secondary buttons need enough restraint to defer to it. No formula captures this; specimens do.

D1. Maximum Chroma Per Hue

The sRGB gamut is not a uniform shape. Some hues can be pushed much further in chroma than others at the same lightness. If you try to use the same chroma value across all your semantic colors, some will be vivid and others will be gamut-clipped into something unexpected.

This matrix shows six chroma levels across eight hue families, all at L:0.55 (a lightness suitable for white text on most hues). Cells that would fall outside the sRGB gamut are marked — the browser will clip them to the nearest representable color, producing unpredictable results.

Chroma × Hue at L:0.55 · gamut-clipped cells marked ⚠
C: 0.05
C: 0.10
C: 0.15
C: 0.20
C: 0.25
C: 0.30
Red (25°)
Aa
Aa
Aa
Aa
Orange (55°)
Aa
Aa
Aa
Yellow (90°)
Aa
Aa
Green (145°)
Aa
Aa
Aa
Cyan (195°)
Aa
Aa
Blue (260°)
Aa
Aa
Aa
Aa
Aa
Purple (310°)
Aa
Aa
Aa
Aa
Pink (350°)
Aa
Aa
Aa
Aa

Blue has the deepest gamut at this lightness — it can be pushed to C:0.25 before clipping. Yellow and cyan clip first, around C:0.13–0.15. This means you cannot use a single chroma value for all your semantic colors and expect them to look equally vivid. Instead, you need to pick a target perceptual intensity and find the maximum chroma each hue supports at that intensity, stopping short of the gamut boundary.

The practical rule: for a multi-color button system, choose your chroma values per-hue. A good starting point is 75–85% of each hue's maximum sRGB chroma at your target lightness. This keeps you safely inside the gamut while producing vivid, confident color. Tools like the Evil Martians OKLCH picker and oklch.fyi visualize the gamut boundary in real time.

E1. Chroma for Text: A Different Standard

Text color and fill color have fundamentally different chroma requirements. A button fill occupies a large visual area — even low chroma is perceptible. Text is thin, rendered at small sizes, and competes with the surrounding fill. It needs higher contrast and lower chroma to remain legible.

For text on a colored button fill (the button label), chroma should be near zero — white (#fff) or near-black (#1a1a1a). Colored button text is almost never appropriate for filled buttons because the chromatic text color will interfere with legibility against the chromatic background. The exceptions are outlined and ghost buttons, where the text color carries the semantic signal.

For chromatic text on a neutral background (outlined/ghost button labels), chroma needs to be high enough that the color reads clearly at text size, but the lightness must be chosen to meet contrast ratios against the background.

Blue text at different chromas on white · which reads as "interactive"?
C: 0.04
C: 0.08
C: 0.14
C: 0.20

At C:0.04, the text looks dark gray — the blue is imperceptible at text size. At C:0.08, a blue tint is faintly visible. At C:0.14, the text is clearly blue — this is the lower threshold for chromatic text that communicates a semantic role. At C:0.20, the blue is vivid and unmistakable.

The minimum chroma for chromatic button text is approximately 0.10–0.12. Below that, the color is not perceivable at 14px body text size and the semantic signal is lost. Above 0.18, the text becomes visually dominant and can compete with filled primary buttons for attention.

Red text carries special weight

Red danger text · chroma levels for destructive actions
C: 0.08 · subtle
C: 0.14 · clear
C: 0.20 · alarming

Red at even modest chroma levels carries strong semiotic weight — "danger," "error," "stop." A destructive text button at C:0.14 reads clearly as a warning. At C:0.20 it feels alarming, which may be appropriate for confirmation dialogs but too aggressive for inline list actions. The right chroma for danger text depends on context: understated in tables and lists (C:0.12–0.15), assertive in confirmation dialogs (C:0.16–0.20).

F1. Chroma in Ghost and Tinted Fills

Ghost buttons use a lightly tinted fill derived from the semantic color. The chroma of this tint determines whether the button reads as "lightly colored" or "nearly invisible." Too little and it vanishes against the page background. Too much and it competes with filled buttons.

Blue ghost button · tint fill at different chromas (L:0.94)
C: 0.01 · barely there
C: 0.025 · subtle
C: 0.04 · visible
C: 0.07 · strong

The opacity-based approach from Part II sidesteps this by using the base color at 6–10% opacity over the surface. But when defining tokens explicitly, the tint fill's chroma needs to be approximately 0.02–0.05 at high lightness (L:0.92–0.96). Below 0.02, the tint is imperceptible on most monitors. Above 0.06, the fill becomes a strong visual element that shifts the button's perceived hierarchy upward.

Red ghost fills deserve extra caution — even at very low chroma, a pink-tinted background carries emotional weight:

Red ghost button · tint fill chromas (L:0.94)
C: 0.015
C: 0.03
C: 0.05

Even at C:0.03, the red tint is clearly visible and signals danger. This is significantly lower than the threshold for blue. Red requires less chroma to communicate because the hue itself carries strong cultural meaning. The general principle: semantically loaded hues (red, green) need less chroma than neutral hues (blue, purple) to achieve the same communicative impact.

G1. Saturation in Dark Mode

Saturated colors that work perfectly on light backgrounds become visually aggressive on dark ones. The effect is physiological: the eye's adaptation to a dark surround makes any color appear more vivid, and highly saturated hues on dark surfaces produce a perceptual "vibration" — a shimmering instability at the edges of colored elements that causes eye strain.

Light mode · standard chroma C: 0.16–0.20 · comfortable
Dark mode · same chroma, higher lightness Same C · feels louder

The right-side buttons maintain the same chroma but increase lightness for visibility on dark backgrounds. The result is they feel more intense, more neon, more fatiguing. This is the vibration problem.

Light mode · standard C: 0.16–0.20
Dark mode · desaturated C reduced 30–40% · comfortable

The correction: reduce chroma by 25–40% when shifting to dark mode. Material Design codifies this as moving from tone-600 (higher chroma) in light mode to tone-200 (lower chroma, higher lightness) in dark mode. The resulting colors are pastels — softer, less saturated versions of the light mode fills. They feel lighter and gentler, which is exactly what the eye needs against a dark surround.

The exception: small, high-emphasis elements

Material Design permits one exception to the desaturation rule: small, high-emphasis interactive elements like FABs and primary buttons may retain the light-mode chroma level even in dark mode. The rationale is that at small surface area, the vibration effect is minimal and the saturated color helps the CTA stand out against a field of muted dark surfaces. If you adopt this exception, limit it to a single element per screen — one saturated accent button in a sea of desaturated dark mode surfaces.

The vibration test. Open your dark mode design on a real device in a dim room. Stare at a saturated colored button for five seconds. If the edges appear to shimmer or glow, or if your eyes feel strained, the chroma is too high. Reduce it until the effect disappears. This is not measurable by any formula — it is a perceptual phenomenon that requires visual inspection.

H1. Blue Text, Chromatic Aberration, and the Eye

The human eye focuses different wavelengths of light at slightly different distances — a phenomenon called chromatic aberration. Blue light focuses farther from the retina than red or green. This means that saturated blue text on dark backgrounds can appear to shimmer or blur at the edges, even for users with normal vision.

Saturated colored text on dark background · shimmer risk
Blue text High C blue · shimmer
Red text High C red · shimmer
Blue text Lower C blue · calm
Red text Lower C red · calm

The WCAG 3.0 draft directly addresses this: pure blue text on black and pure red text on black should be avoided at high saturation because the eye literally cannot focus both channels simultaneously. Desaturating the text or lightening it (reducing chroma, increasing lightness) resolves the issue. For dark mode button labels that use chromatic text, keep chroma moderate (C:0.08–0.14) and lightness high (L:0.75–0.85).

This is another case where fills and text diverge: a saturated blue button fill on a dark background is acceptable (the fill is a large area, and edges are clear). But saturated blue text on a dark background is problematic because text is thin and the chromatic aberration manifests at the fine edges of letterforms.

I1. Chroma as a Hierarchy Signal

Chroma is a hierarchy lever, just like fill mode, size, and weight. Higher chroma commands more attention. Lower chroma recedes. This means you can use chroma variation within a single hue to express the same hierarchy that fill mode (filled → outlined → ghost → text) expresses structurally.

Hierarchy through chroma alone · same hue, same lightness, different intensity
C: 0.22 · vivid
C: 0.12 · muted
C: 0.05 · near-gray

In practice, this is rarely used as the primary hierarchy mechanism because fill mode is more robust and more accessible (it provides shape contrast, not just color contrast). But chroma variation is an excellent secondary hierarchy signal. A secondary outlined button with slightly lower chroma text (C:0.12 vs. the primary's C:0.18) naturally defers to the primary without changing shape or layout.

Cross-semantic chroma matching

When your primary blue button and your danger red button coexist on the same screen, they should feel like they belong to the same system. If the blue is at C:0.20 and the red is at C:0.12, the red will feel meek — not an appropriate signal for a destructive action. If the red is at C:0.22 and the blue at C:0.16, the red will dominate visually even when it is supposed to be secondary.

Cross-semantic chroma matching
Mismatched · red feels weak
Mismatched · red dominates
Matched · equal weight

When two semantic colors appear at the same hierarchy level (both filled), they should have chromas that produce equal perceptual intensity. OKLCH makes this straightforward: use the same C value for both, and the perceptual uniformity of the space ensures they feel equivalent. Adjust slightly if one hue hits the gamut boundary — always prefer the lower of the two available maxima.

J1. Chroma for Borders and Outlines

Borders are the thinnest color element on a button. At 1–2px, color is barely perceivable. This means border chroma needs to be higher relative to its opacity to register as "colored" rather than "gray."

1.5px outlined button · border chroma at varying levels
C: 0.04 · gray border
C: 0.10 · tinted
C: 0.18 · colored
C: 0.18 full · strong

A thin border at 40% opacity and C:0.04 reads as gray regardless of hue. At C:0.10 with the same opacity, a faint color tint is visible. At C:0.18, the border clearly reads as blue. The opacity-chroma interaction is multiplicative: you need either high chroma or high opacity (or both) for a thin border to register as chromatic.

Most design systems use one of two strategies: neutral borders with chromatic text (the text carries the semantic signal; the border is structural), or fully chromatic borders derived from the text color at reduced opacity (border: oklch(from var(--text-color) l c h / 0.3)). The second approach creates a more cohesive color relationship within the button, but the first is simpler and avoids the chroma-at-thin-size legibility issue entirely.

K1. Recommended Chroma Values by Role

Synthesizing the preceding sections, here is a reference table of recommended OKLCH chroma ranges for each surface in a button system.

SurfaceLight mode chromaDark mode chromaNotes
Primary filled button 0.14–0.22 0.09–0.15 The hero CTA. Vivid enough to dominate the visual hierarchy. Reduce 25–40% for dark mode.
Danger filled button 0.14–0.20 0.09–0.14 Match primary's perceptual intensity. Red's semiotic weight means it can be slightly lower chroma and still feel equally assertive.
Chromatic text (outlined/ghost) 0.12–0.18 0.08–0.14 Must be high enough to read as colored at text size. Minimum ~0.10 for hue identity.
Tinted ghost fill 0.02–0.05 0.02–0.04 Very light wash at high lightness (L:0.92–0.96). Red tints register at lower chroma than blue.
Chromatic border (outlined) 0.10–0.18 at 30–50% opacity 0.08–0.14 at 20–40% opacity Thin borders need high chroma or high opacity. Use one or both.
Disabled button fill 0.02–0.05 0.01–0.03 Barely chromatic. Some systems strip chroma entirely (use neutral gray for all disabled buttons regardless of original color).
Neutral button (all variants) 0.00–0.02 0.00–0.02 Near-achromatic. Slight warm (H:50) or cool (H:260) tint is acceptable for brand alignment.

L1. Implementing Chroma as Tokens

OKLCH makes chroma a first-class design token. Because lightness, chroma, and hue are independent axes, you can define your button color system as a set of hue tokens, a set of lightness tokens per theme, and a set of chroma tokens per role — then compose them.

/* Primitive hue tokens */ --hue-primary: 260; --hue-danger: 25; --hue-success: 145; /* Lightness tokens per theme */ --L-filled-light: 0.50; --L-filled-dark: 0.72; --L-text-light: 0.45; --L-tint-light: 0.94; /* Chroma tokens per role and theme */ --C-filled-light: 0.18; --C-filled-dark: 0.11; /* ~40% reduction */ --C-text: 0.15; --C-tint: 0.035; --C-disabled: 0.03; /* Composed semantic tokens */ --btn-primary: oklch( var(--L-filled-light) var(--C-filled-light) var(--hue-primary) ); --btn-danger: oklch( var(--L-filled-light) var(--C-filled-light) var(--hue-danger) ); /* State derivation via chroma + lightness */ .btn-primary:hover { background: oklch( calc(var(--L-filled-light) - 0.05) var(--C-filled-light) var(--hue-primary) ); } .btn-primary:disabled { background: oklch( calc(var(--L-filled-light) + 0.15) var(--C-disabled) var(--hue-primary) ); }

This architecture lets you adjust the chroma of every button in the system by changing a single token (--C-filled-light). Theme switching redefines the L and C tokens; hue stays constant. The separation of hue, lightness, and chroma into independent tokens is only possible because OKLCH's axes are perceptually orthogonal — changing one does not produce unexpected shifts in the others.

M1. The Chroma Decision Sequence

These decisions build on the color palette choices from Part III-A. They refine each color by determining its intensity.

StepDecisionKey constraint
1 Check the maximum sRGB chroma for each of your hues at your target lightness Gamut boundary (varies by hue)
2 Choose primary button fill chroma (0.14–0.22 for light mode) Must feel like the dominant interactive element on the page
3 Match danger chroma to primary's perceptual weight Equal-intensity semantic colors; OKLCH C values can differ if one hue's gamut is smaller
4 Set chromatic text chroma (outlined/ghost labels) ≥ 0.10 Below 0.10, hue identity is lost at body text size
5 Set ghost tint fill chroma (0.02–0.05 at L:0.92–0.96) Must be perceptible but subordinate to filled buttons
6 Set disabled chroma (0.02–0.05 or strip to zero) Should signal "inactive" without becoming invisible
7 Reduce all chromas by 25–40% for dark mode fills Vibration prevention; dark adaptation makes colors feel louder
8 Test chromatic text in dark mode for chromatic aberration Saturated blue/red text shimmers on dark backgrounds; reduce C, increase L
9 Validate perceptual hierarchy: primary > danger > secondary > ghost > disabled Visual inspection only — no formula captures this
10 Perform the vibration test on a real device in a dim room Eye strain and edge shimmer on saturated dark-mode elements
The test for saturation success. Place your primary filled button, your danger outlined button, your neutral ghost button, and your disabled button in a row — in both light and dark mode. If the chroma progression creates a clear visual hierarchy from most-intense to least-intense, the system works. If two non-primary buttons compete for attention because of mismatched chroma, revisit steps 3–5.

Part III-B of the Design Primitive Reference series · Continues from Part III-A (Color Palette for Buttons) · April 2026