01

Treat every button state as a complete color pair

Start from semantic state roles rather than one brand swatch. A filled primary button has at least a label, fill, optional border, icon, and surrounding surface. Hover and active states may change fill, while focus adds an indicator that can touch both the control and the page. A secondary or outlined button creates another set of adjacent colors. Record each foreground, background, and boundary explicitly. Do not infer that a darker hover color passes because the default color passed: the text ratio, boundary visibility, and focus relationship must be recalculated for the actual rendered state.

  • List default, hover, active, focus, disabled, loading, and selected states that the component can actually render.
  • Record the label, icon, fill, border, focus indicator, and surrounding surface for every supported variant.
  • Treat primary, secondary, destructive, and quiet buttons as separate contracts rather than recoloring one sample.
  • Keep the native button semantics so keyboard operation and accessible naming do not depend on the visual palette.
02

Measure text, boundary, and focus for their own purposes

Button label text is evaluated against the rendered button fill. Under WCAG 2.2 criterion 1.4.3, ordinary text generally requires at least 4.5:1, while qualifying large text uses 3:1, subject to the criterion’s definitions and exceptions. Non-text contrast concerns visual information needed to identify user-interface components and states; where a fill or border supplies that information, test it against adjacent colors under criterion 1.4.11. Focus indicators have separate requirements and must remain perceivable in the focused state. One ratio cannot certify all three jobs, because each comparison uses different pixels and meaning.

PartCompare withQuestion
Label or iconRendered fillIs the content readable?
Essential boundaryAdjacent surfaceCan the control be identified?
Focus indicatorColors it touchesCan keyboard focus be located?
State changePrevious and current presentationIs the interaction feedback perceivable?
03

Implement states without losing the focus indicator

The example uses explicit colors so every pair can be measured. It is illustrative rather than a universal compliant palette: font properties, surrounding surfaces, browser rendering, and component variants still require testing. :focus-visible can present a strong keyboard-style focus indicator in supporting browsers, while the supported-browser policy should preserve an appropriate fallback. The outline is offset so it does not merge with the border, but it can still touch several backgrounds in a dense layout. Never remove the user-agent outline unless the replacement is at least as visible throughout every component state and clipping context.

css
.button {
  color: #ffffff;
  background: #4338ca;
  border: 2px solid #4338ca;
}

.button:hover { background: #3730a3; }
.button:active { background: #312e81; }
.button:focus-visible {
  outline: 3px solid #6d28d9;
  outline-offset: 3px;
}

.button[disabled] {
  color: #64748b;
  background: #e2e8f0;
  border-color: #cbd5e1;
}
04

Design hover, active, loading, and disabled states deliberately

Hover is pointer feedback and cannot replace keyboard focus. Active or pressed feedback may be brief, but the label must not disappear during activation. A loading state should retain an accessible name and communicate progress without relying only on a color shift; avoid layout movement when a spinner appears. Disabled controls have exceptions in some contrast criteria, yet making them extremely faint can prevent users from understanding available actions or why submission is blocked. Consider whether the control should be disabled at all, and provide nearby validation or explanatory text when a prerequisite is unmet.

  • Recalculate label contrast after every fill change; do not assume darker always means safer for all foregrounds.
  • Keep hover feedback for pointers and a separately visible focus indication for keyboard navigation.
  • Pair loading visuals with a programmatic state and stable accessible name rather than a color animation alone.
  • Use disabled styling only for genuinely unavailable actions and explain recoverable prerequisites nearby.
  • Check selected or pressed states with semantics such as aria-pressed when the button represents a toggle.
05

Account for alpha, gradients, icons, and nested surfaces

Translucent fills and borders do not have a single final color independent of the page. Composite them over every permitted surface before calculating contrast. A gradient button requires checking relevant positions behind the label and icon, not only its endpoints. Icons that communicate the action need sufficient visibility and an accessible name through visible text or another supported mechanism; decorative icons should not add redundant announcements. When a button appears inside a banner, card, dialog, image overlay, or disabled container, use that actual surrounding color for boundary and focus checks. Shared tokens are valid only for their documented contexts.

06

Run a component matrix before release

Render every button variant on each supported surface with realistic labels, icons, wrapping, and localization. Navigate by keyboard, pointer, touch, and assistive technology appropriate to the test plan. Inspect focus at normal zoom and increased browser zoom, including buttons near container edges where overflow may clip an outline. Exercise high-contrast or forced-colors settings and verify that native semantics still expose controls and states. Automated contrast checks are valuable for deterministic pairs, but gradients, alpha, dynamic backgrounds, animation, and clipping need rendered inspection. Store the state matrix beside the tokens so later palette changes rerun the same evidence.

  • Render primary, secondary, quiet, destructive, icon-only, and toggle variants that exist in the product.
  • Exercise default, hover, active, focus, loading, disabled, error-adjacent, and selected states.
  • Test every supported surface, including cards, dialogs, banners, tables, and image-backed regions.
  • Repeat contrast and focus checks after typography, spacing, border, shadow, or shared token changes.
S

Primary sources

The factual claims in this article are checked against these published specifications.

  1. W3C WCAG 2.2 — Contrast (Minimum)
  2. W3C WCAG 2.2 — Non-text Contrast
  3. W3C WCAG 2.2 — Focus Appearance (Level AAA)
  4. W3C WAI-ARIA 1.2 — aria-pressed state