Color & design
Design Color Contrast into Reusable Web Components
Build foreground and surface tokens as tested pairs, verify button states and themes, and keep decorative palette choices separate from readable content.
Design permitted color pairs rather than isolated swatches
A palette can look balanced while producing unreadable text when its colors are combined. Define roles such as page surface, primary text, secondary text, button fill and button label. Then document which foreground belongs on which surface. A swatch is not inherently accessible: its contrast depends on the color beside or behind it and the role of the information being conveyed.
Start with a small component inventory. A card can need a heading, description, link, badge and button, each with its own background relationship. Listing those combinations early helps prevent the same muted gray from being applied indiscriminately to body copy, disabled controls and decorative rules. The visual hierarchy should remain clear without making useful text too faint.
Create a button whose states have measured pairs
This example uses white text on a dark blue button, with a darker blue hover state. Set #ffffff against #1d4ed8 in the contrast checker: the ratio is about 6.70:1. Change the background to #1e40af for the hover state: it becomes about 8.72:1. Both pairs exceed the ordinary-text AA threshold.
:root {
--surface: #ffffff;
--text: #111111;
--text-secondary: #595959;
--action: #1d4ed8;
--action-hover: #1e40af;
--on-action: #ffffff;
}
.action-button {
font: 600 1rem/1.4 system-ui, sans-serif;
padding: .65rem 1rem;
color: var(--on-action);
background: var(--action);
border: 2px solid var(--action);
border-radius: .4rem;
}
.action-button:hover {
background: var(--action-hover);
border-color: var(--action-hover);
}
.action-button:focus-visible {
outline: 3px solid #111111;
outline-offset: 3px;
}
Apply the class to an actual button element so its keyboard behavior and semantics come from the right control. This style example assumes a white surrounding surface. The focus outline is a separate visual feature from the label, so review its visibility in the real layout rather than treating the label’s contrast ratio as evidence about focus.
Keep secondary text readable on its assigned surface
The secondary text token #595959 has a ratio just above 7:1 on white. It is visually softer than #111111 while remaining suitable for ordinary explanatory copy in this example. A much paler neutral can be appropriate for decoration yet fail when reused for a small caption. Distinguish those roles in both token names and component documentation.
Do not lower opacity on a whole card just to de-emphasize its text. That also changes its descendants and their relationship to the surface beneath the card. Choose an explicit foreground token for the intended role, and inspect any transparency or overlays in the final rendering. The local checker accepts opaque colors and does not model a stack of translucent layers for you.
Define dark-theme pairs independently
A color that passes on white is not automatically suitable on a dark surface. For example, #767676 is about 4.54:1 on white but only about 4.16:1 against #111111, which fails ordinary-text AA. Merely swapping the page background while retaining every text token can create a regression. Define foreground-on-surface relationships for each theme and test them as separate pairs.
Theme changes can also affect borders, selected states and icons. Prefer semantic variables such as on-action or text-on-card over names that encourage arbitrary reuse of one shade everywhere. If users can choose custom colors, preserve a way to return to a supported readable configuration and avoid assuming every arbitrary pair has been reviewed.
Review the component’s complete state matrix
Include normal, hover, focus, selected, error and disabled presentations where they exist. A selected tab can communicate its state through an underline, a label and a programmatic state rather than hue alone. A validation message should explain the error, not just change the input border to red. Those design decisions make the interface more understandable even when colors are not perceived as intended.
Test narrow layouts and longer labels. A gradient, image crop or responsive font size can change the applicable comparison. Do not lock a heading to one line simply to preserve a carefully chosen background region. When a variable image makes text contrast unpredictable, an opaque label surface can make the component more robust.
Keep the tested relationships with the component
Store the color pairs, ratios and relevant text sizes alongside the component specification. Recheck them when a token or state changes, and verify the rendered implementation after applying design-file updates. The W3C text criterion and non-text criterion explain the different comparisons. The palette supports accessibility when these relationships survive actual use, not merely when its swatches look attractive in isolation.


