Non-text contrast: UI, icons & charts

Text isn’t the only thing on a page that has to be seen. The edge that tells you where an input ends, the slice of colour that distinguishes one line on a chart from the next, the icon that means “delete”, the ring that shows which control has keyboard focus — all of these carry meaning through their appearance alone. If they don’t stand out from what surrounds them, people with low vision, age-related sight loss, or a washed-out screen in bright light can’t tell the boundary, the category, or the current position. That is exactly what WCAG success criterion 1.4.11 Non-text Contrast guards against.

The rule is a single number: the visual information needed to identify a user interface component or a meaningful graphic must have a contrast ratio of at least 3:1 against the colour next to it. This lesson works through the three places teams most often miss it — control borders, data and icon colours, and focus indicators — and shows the small palette changes that bring each one back into conformance.

What you’ll learn

What the 3:1 threshold in 1.4.11 actually covers and what it doesn’t; how to give input and button boundaries enough contrast against the page; how to choose chart series colours and meaningful-icon colours that clear 3:1 against their background and each other; and how to make a focus indicator visible against every colour it lands on.

Standards this lesson maps to
Standard Criterion Level What it requires
WCAG 2.2 1.4.11 Non-text Contrast AA The visual information needed to identify UI components and states, and meaningful graphical objects, has a contrast ratio of at least 3:1 against adjacent colour.
WCAG 2.2 2.4.11 Focus Not Obscured (Minimum) AA A focused component is at least partly visible — pairs with a visible focus indicator so focus can actually be located.
WCAG 2.2 2.4.7 Focus Visible AA Any keyboard-operable interface has a visible focus indicator; 1.4.11 then governs that indicator’s contrast.
EN 301 549 9.1.4.11 (incorporates WCAG) European harmonised standard; references the WCAG AA set including Non-text Contrast.
Section 508 502.3 / 504 (incorporates WCAG A & AA) US federal ICT must meet WCAG 2.0 Level A and AA; 1.4.11 is part of the harmonised 2.1/2.2 update many agencies apply.
ADA Title II WCAG 2.1 AA (DOJ rule) AA US state/local government web content must conform to WCAG 2.1 AA, which includes 1.4.11.

The three problems we’ll fix

Each card below isolates one common non-text-contrast defect. For every issue you get a plain-language statement of the problem, a Bad example (shown as escaped, non-running code so it can’t harm this page), a Good example, the copyable Code, and an ordered fix. All ratios refer to 1.4.11’s 3:1 minimum.

Input & button borders below 3:1 against the page

WCAG 2.2 · 1.4.11 AA EN 301 549 Section 508 ADA Title II

The boundary that tells a person “this is a field you can type in” or “this is a button you can press” is itself visual information that identifies a component, so 1.4.11 applies to it. A pale grey hairline on a white card looks tasteful in a mockup but often measures around 1.4:1 — far below the 3:1 minimum. For a low-vision user the input becomes invisible: it reads as empty space, not as something to interact with. The same trap catches “ghost” and outline buttons whose only border is a faint tint, and tonal buttons whose fill is barely lighter than the page. Note the boundary needs contrast against adjacent colour — the page or card behind it — not against the label inside.

Bad

A near-white border on a white form. The label text passes 1.4.3, but the field’s edge is roughly 1.4:1 against the page, so the control itself can’t be located (1.4.11).

bad-input-border.css
/* page background is #ffffff */
.field {
  background: #ffffff;
  border: 1px solid #ececec; /* ~1.2:1 vs #fff — fails 1.4.11 */
}
.btn-ghost {
  background: #ffffff;
  border: 1px solid #e4e4e4; /* ~1.3:1 vs #fff — fails */
  color: #1a1a1a;
}

Good

The border is darkened until it clears 3:1 against the page. A 1px #767676 edge measures about 4.5:1 on white, comfortably over the minimum, and still reads as a quiet UI line.

good-input-border.css
/* page background is #ffffff */
.field {
  background: #ffffff;
  border: 1px solid #767676; /* ~4.5:1 vs #fff — passes 1.4.11 */
}
.btn-ghost {
  background: #ffffff;
  border: 1px solid #595959; /* ~7:1 vs #fff — passes */
  color: #1a1a1a;
}

Code

Drive boundaries from a token so every theme inherits a conforming value, and remember the rule cuts both ways: on a dark surface the border must be light enough to clear 3:1. A solid-fill button passes via its fill-to-page contrast, so it doesn’t need a border at all.

control-border-tokens.css
:root        { --control-border: #767676; } /* on light: ~4.5:1 vs #fff */
[data-theme="dark"] { --control-border: #8a8a8a; } /* on #1a1a1a: ~3.6:1 */

.field, .btn-outline { border: 1px solid var(--control-border); }

/* Solid button: identified by its fill, not a border */
.btn-primary { background: #1d4ed8; color: #fff; border: 0; } /* fill ~3.7:1 vs #fff page */

How to fix

  1. Measure each control’s boundary against the colour directly behind it — the page or card — and require at least 3:1.
  2. Darken hairline borders (a value around #767676 on white) or give the control a fill that itself clears 3:1 against the page.
  3. For outline and ghost buttons, treat the outline as the identifying information; if it’s decorative tint only, it will fail.
  4. Define the border as a token and check it in every theme — light, dark, and high-contrast all need their own conforming value.
  5. Don’t rely on the placeholder or inner shadow to convey “this is a field”; those aren’t guaranteed to meet 3:1.

Chart series & meaningful icons below 3:1

WCAG 2.2 · 1.4.11 AA EN 301 549 Section 508

1.4.11 covers “graphical objects required to understand the content”. In a chart, the coloured line, bar, or slice is the data, so each series must clear 3:1 against the plot background. When two adjacent series are also told apart only by colour, they must additionally differ from each other by 3:1, or a reader can’t separate them. The same applies to a meaningful icon — a warning triangle, a “required” asterisk-mark, a status dot: if the shape carries information and isn’t backed by a text label, its colour must reach 3:1 against what’s behind it. Purely decorative graphics are exempt, but anything load-bearing is not. Note that 1.4.11 governs identification, not data values, so pairing colour with shape, pattern, or direct labels is the most robust route.

Bad

Pastel series on a white plot. The lightest line sits near 1.6:1 against the background and two neighbours differ by under 3:1, so neither the line nor which series it is can be made out.

bad-chart-series.css
/* plot background #ffffff */
--series-1: #bfdbfe; /* ~1.3:1 vs #fff — fails */
--series-2: #bbf7d0; /* ~1.3:1 vs #fff — fails */

<!-- meaningful icon, colour only, no label -->
<svg class="status" aria-hidden="true">
  <circle fill="#fca5a5"/> <!-- ~1.8:1 vs #fff: error state unreadable -->
</svg>

Good

Series colours are deepened so each clears 3:1 against the plot and adjacent pairs differ by 3:1. The status icon’s colour passes too — and it gets an accessible name so it isn’t colour-dependent.

good-chart-series.css
/* plot background #ffffff */
--series-1: #1d4ed8; /* ~5.2:1 vs #fff */
--series-2: #047857; /* ~4.5:1 vs #fff; >3:1 vs series-1 */

<svg class="status" role="img" aria-label="Error">
  <circle fill="#b91c1c"/> <!-- ~5.9:1 vs #fff — passes 1.4.11 -->
</svg>

Code

Don’t lean on contrast alone to separate series — add a second channel so the chart also survives colour-blindness and greyscale printing. Dashes, markers, or direct labels make each line identifiable independent of hue.

redundant-encoding.css
<!-- colour (3:1) + dash pattern + end label = identifiable three ways -->
<path d="…" stroke="#1d4ed8" stroke-dasharray="0"/>     <!-- Revenue -->
<path d="…" stroke="#047857" stroke-dasharray="6 4"/>  <!-- Costs -->
<text x="…" y="…" fill="#1d4ed8">Revenue</text>
<text x="…" y="…" fill="#047857">Costs</text>

How to fix

  1. Check every chart series against the plot background and require at least 3:1; deepen pastel palettes until they pass.
  2. Where series are told apart by colour alone, also require 3:1 between adjacent series so they’re separable.
  3. Give any meaningful icon a colour that clears 3:1 against its background, and an accessible name (role="img" + aria-label) so it isn’t colour-only.
  4. Add a second visual channel — pattern, marker, or direct label — so the graphic survives colour-blindness and greyscale.
  5. Leave genuinely decorative graphics out of scope, but be honest about what’s load-bearing.

Focus ring below 3:1 against adjacent colours

WCAG 2.2 · 1.4.11 AA 2.4.7 AA EN 301 549 ADA Title II

A focus indicator is the keyboard user’s “you are here”. 2.4.7 requires it to be visible, and 1.4.11 requires the indicator to reach 3:1 against the colours next to it. The catch is the word adjacent: a ring sits between the component and the page, so it can need 3:1 against both. A subtle blue glow may clear 3:1 against a white page but vanish against a blue button; a thin light outline may show on a dark control but disappear on the dark surface around it. Removing the indicator entirely with outline: none is the most common failure of all — it deletes the only cue a keyboard user has.

Bad

The default outline is suppressed and replaced with a faint ring that only shows against the page. On the primary button — a similar blue — the indicator drops well under 3:1 and disappears.

bad-focus.css
:focus { outline: none; } /* removes the only keyboard cue — fails 2.4.7 */

.btn:focus-visible {
  outline: 2px solid #93c5fd; /* light blue: <3:1 vs the blue button it rings */
}

Good

A solid ring with an offset so a sliver of page shows between ring and control. A dark ring clears 3:1 against both the light page and the button; the offset guarantees separation regardless of the component’s colour.

good-focus.css
.btn:focus-visible {
  outline: 3px solid #1a1a1a;   /* ~16:1 vs page, ~5:1 vs the button */
  outline-offset: 2px;          /* page shows through — separates ring from control */
}

Code

A two-tone ring is bullet-proof: a light core and a dark edge means one of the two always clears 3:1 whatever it lands on. Scope it to :focus-visible so it appears for keyboard use without flashing on every mouse click.

two-tone-focus.css
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid #ffffff;          /* light edge for dark surfaces */
  outline-offset: 2px;
  box-shadow: 0 0 0 4px #1a1a1a;       /* dark edge for light surfaces */
}
/* One of the two layers always clears 3:1 against the adjacent colour */

How to fix

  1. Never ship outline: none without a replacement indicator — a missing focus ring fails 2.4.7 outright.
  2. Check the indicator against both adjacent colours, the component and the page, and require 3:1 against each.
  3. Use outline-offset so a slice of background separates the ring from the control, keeping it visible on any fill.
  4. Prefer a two-tone (light + dark) ring so one layer always clears 3:1 whatever surface the control sits on.
  5. Scope it to :focus-visible so keyboard users get a clear indicator without it firing on every pointer click.

Recap

  • Give the boundary that identifies an input or button at least 3:1 against the page behind it — a faint hairline border fails 1.4.11 even when the label text passes 1.4.3.
  • Make meaningful graphics carry their meaning at 3:1: chart series must clear 3:1 against the plot background and be distinguishable from each other, and an icon that conveys information must clear 3:1 against what’s behind it.
  • Ensure the focus indicator reaches 3:1 against every colour it can land on — the component and the page — so keyboard users can always see where they are (1.4.11 with 2.4.7).

The 3:1 rule is the same wherever non-text information lives. Hit it for borders, graphics, and focus and you satisfy WCAG, EN 301 549, Section 508, and ADA Title II together.