Accessibility Recipe No. 01

The two-tone focus ring

One focus style that keeps at least 3:1 contrast on every solid background. Here is the recipe, the maths that makes it work, and the five places it still fails.

Updated October 2026Reviewed by Khushwant Parihar, CPACC

One ring, two backgrounds. On white the black band shows; on black the white band does. The button is #767676, the grey where black and white come closest to a tie.
Serves
Any solid background
Prep
Two CSS declarations

Taste test: Tab across twelve backgrounds

Press Tab to walk the wall. Start with the single blue ring, the style most sites ship, including this one. It reaches 3:1 on only 3 of the 11 solid colours: Paper, Ink and Sunflower. On Focus blue it is gone entirely. Then switch to the two-tone ring and walk it again.

Each tile lists both rings' contrast against it. The minimum for a focus indicator is 3:1.

Focus style on the wall
  • Paper#ffffff
    Blue ring
    5.16:1 passes
    Two-tone
    21.00:1 passes
  • Ink#000000
    Blue ring
    4.06:1 passes
    Two-tone
    21.00:1 passes
  • Tie-break grey#767676
    Blue ring
    1.13:1 fails
    Two-tone
    4.62:1 passes
  • Focus blue#2563eb
    Blue ring
    1.00:1 fails
    Two-tone
    5.16:1 passes
  • Navy#1e3a8a
    Blue ring
    2.00:1 fails
    Two-tone
    10.35:1 passes
  • Sunflower#facc15
    Blue ring
    3.37:1 passes
    Two-tone
    13.71:1 passes
  • Tomato#ef4444
    Blue ring
    1.37:1 fails
    Two-tone
    5.58:1 passes
  • Mint#34d399
    Blue ring
    2.68:1 fails
    Two-tone
    10.92:1 passes
  • Violet#7c3aed
    Blue ring
    1.10:1 fails
    Two-tone
    5.69:1 passes
  • Tangerine#f97316
    Blue ring
    1.84:1 fails
    Two-tone
    7.49:1 passes
  • Charcoal#1f2937
    Blue ring
    2.84:1 fails
    Two-tone
    14.67:1 passes
  • Fadegradient

    Not one solid colour, so no guarantee: check this one by eye.

Why it can't lose

Every colour has a contrast ratio against white and another against black. Multiply the two and you always get 21, the contrast between white and black themselves.

If two numbers multiply to 21, they cannot both be small. The worst case is when they are equal, at the square root of 21: 4.58:1. So on any solid background, the better band of a black and white ring has at least 4.58:1, comfortably over the 3:1 a focus indicator needs.

The working

against white = 1.05 ÷ (L + 0.05)
against black = (L + 0.05) ÷ 0.05
white × black = 1.05 ÷ 0.05 = 21
worst case = √21 ≈ 4.58

L is the background's relative luminance, from 0 for black to 1 for white. It cancels out, which is the whole trick.

Now try to break it. Drag the sliders, type any hex colour, or load a tile from the wall. The single blue ring fails all over the place. The two-tone ring never drops below 3:1, and the last line never moves off 21.

Both buttons are drawn as if focused. You can also Tab to them.

Background
#2563eb
Blue ring against it
1.00:1Fail
White band against it
5.16:1
Black band against it
4.06:1
Better band
5.16:1Pass
White × black
21.00

The recipe

Ingredients

  • One outline for the inner band.
  • One box-shadow for the outer band, reaching 2px further.
  • Two colours at least 9:1 apart. Black and white are 21:1.

Method

  1. Put it on :focus-visible, so keyboard focus always shows it and a mouse click does not.
  2. Keep outline-offset: 0. Both bands then sit on the background outside the component, which is the only place W3C's guarantee covers.
  3. Make each band at least 2px thick. That also meets the size test in 2.4.13 Focus Appearance.
  4. Never write outline: none to swap in a shadow-only ring. Forced-colours mode removes shadows, and the ring with them.
focus-ring.css
/* The two-tone focus ring (W3C technique C40) */
:focus-visible {
  /* Inner band. An outline survives forced-colours mode. */
  outline: 2px solid #fff;
  /* Both bands sit on the background, outside the component. */
  outline-offset: 0;
  /* Outer band: 2px of black around the white. */
  box-shadow: 0 0 0 4px #000;
}

Serving notes

  • Components that already use box-shadow for elevation lose it on focus. List both shadows, ring first.
  • A global rule loses to component styles. Search the codebase for outline: none, outline: 0 and Tailwind's outline-none.
  • Outlines follow border-radius in current browsers, so the ring hugs pills and rounded cards.
card.css
/* A card that already has a shadow keeps it */
.card:focus-visible {
  box-shadow: 0 0 0 4px #000, 0 8px 24px rgb(0 0 0 / 0.15);
}

Make it on-brand: the 9:1 rule

Black and white are not the only pair. W3C's technique C40 gives the general rule: if the two colours contrast at least 9:1 with each other, at least one of them reaches 3:1 against any solid background.

It is the same maths. The background that hurts most sits halfway between the two colours, where each band gets the square root of the pair's contrast. The square root of 9 is 3. Pick a pair below and the builder shows that worst background and writes the CSS.

The two bands contrast with each other at

10.35:1

Guaranteed. That is at least 9:1, so on any solid background one band reaches at least 3.21:1.

Your ring on its worst background, #8f8f8f

This grey sits halfway between your two colours, so it is the background the pair handles worst.

focus-ring.css
/* Two-tone focus ring: #ffffff and #1e3a8a (10.35:1 apart) */
:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 0;
  box-shadow: 0 0 0 4px #1e3a8a;
}

When the recipe won't save you

The recipe fixes one problem: a focus indicator without enough contrast. These five survive it. Each one has a broken and a fixed version to try with the keyboard.

Failure 01 · 2.4.11 Focus Not Obscured (AA)

It is hiding under a sticky header

A perfect ring is no help if the focused control is under something. Tab to the bottom of each list, then press Shift and Tab to come back up. In the broken list each item slides under the sticky bar. The fixed list adds scroll-padding-top equal to the bar's height, so the browser stops scrolling before the bar.

Broken

Sticky header

Fixed

Sticky header

Failure 02 · 1.4.11 Non-text Contrast and 2.4.7 Focus Visible (AA)

Its container cuts it off

The ring is drawn outside the button, so a parent with overflow: hidden clips it. Both buttons below show the ring as if focused. The fix is room: padding on the container at least as wide as the ring (4px here).

Broken

A card with overflow: hidden and a full-width button.

Fixed

A card with overflow: hidden and a full-width button.

Failure 03 · 2.4.7 Focus Visible (AA)

Forced-colours mode deletes half of it

Windows high contrast mode and other forced-colours modes remove every box-shadow. A ring made only of shadows vanishes completely. The recipe keeps one band as an outline, which survives in the user's own colours. The switch below imitates the mode; to test the real thing, use Chrome DevTools, Rendering, Emulate CSS media feature forced-colors.

Broken

Fixed

Failure 04 · 2.4.7 Focus Visible (AA)

Focus lands on something invisible

Custom checkboxes usually hide the real input and draw a box next to it. Focus goes to the hidden input, so the ring is drawn around nothing. Tab to each checkbox. The fix moves the ring to the visible box with input:focus-visible + .box.

Broken

Fixed

Failure 05 · 2.4.7 Focus Visible (AA)

Focus never reaches the option

In a listbox or combobox built with aria-activedescendant, keyboard focus stays on the container and the arrow keys only change which option is active. The CSS ring therefore circles the whole list. Tab into each list and use the arrow keys: the broken one marks the active option with a pale tint, the fixed one gives it the two-tone ring too.

Broken

Pick a herb (broken)

  • Basil
  • Coriander
  • Mint
  • Parsley

Fixed

Pick a herb (fixed)

  • Basil
  • Coriander
  • Mint
  • Parsley

Taste it before you serve it

  1. Tab through every page template with the keyboard alone. Every stop shows the ring.
  2. Search for outline: none, outline: 0 and outline-none. Each one needs a reason or a replacement.
  3. Check the ring over photos, gradients and video by eye. The guarantee covers solid colours only.
  4. Turn on forced colours (Windows contrast themes, or the DevTools emulation) and Tab again.
  5. Shift and Tab backwards through long pages and scroll areas. Nothing focused may hide under a sticky header or banner.
  6. Zoom to 200% and 400%. The ring should grow with the page and stay unclipped.

Questions

Does a two-tone focus ring meet WCAG?

It is W3C's sufficient technique C40 for 1.4.11 Non-text Contrast (for the focus state), 2.4.7 Focus Visible and 2.4.13 Focus Appearance, provided each band is at least 2px thick, the two colours contrast at least 9:1 with each other, and the ring sits over one solid background. It does nothing for 2.4.11 Focus Not Obscured or for the order focus moves in.

Why not use a thicker single-colour ring instead?

Thickness does not fix contrast. Every single colour has backgrounds it disappears against: a 2px or a 6px ring in #2563eb is still 1:1 on a #2563eb button bar. Two bands that contrast with each other cannot both disappear at once.

Which band should go on the inside?

Either works for contrast. Make the inner band the outline and the outer band the box-shadow, as in the recipe, because the outline is the part that survives forced-colours mode, where the browser repaints it in the user's own colour.

Does it work on photos and gradients?

Not with a guarantee. The maths covers one solid colour at a time. On a photo or gradient each band may cross a colour close to its own. The two bands still contrast 21:1 with each other, which usually keeps the ring visible, but check those places by eye.

More on focus

Sources

All Accessibility Recipes