ContentsAct II · MakeDesign it yourself
Move 25
Make focus beautiful
You deleted a working control and rebuilt a broken one. Twice, in two lines, and you have probably done it this month.
Two lines of CSS and HTML do more accessibility damage than anything else developers write, and both are written for aesthetic reasons by people who would never knowingly ship an inaccessible product.
:focus { outline: none; }
<div onClick={handleSave}>Save</div>
The first deletes a focus indicator the browser already provided. The second replaces a
<button> that was already focusable, already activated by Enter and Space, and already named
in the accessibility tree, with an element that is none of those things.
Every line this chapter asks you to write is you buying back a liability you created.
The first one is a named failure, not an opinion
W3C publishes failure techniques, and one of them exists for exactly this. F78 is titled “Failure of Success Criterion 1.4.11, 2.4.7 and 2.4.13 due to styling element outlines and borders in a way that removes or renders non-visible the visual focus indicator.”
Note the plural. It fails three criteria at once. And its own worked code example is :focus {outline: none}, so this is not an interpretation of a rule, it is the rule’s illustration of
itself.
The criterion that binds is 2.4.7 Focus Visible, Level AA: “Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.” That has been AA since WCAG 2.0. It is not new, and it is not negotiable.
Getting the numbers right
Two of the figures usually quoted for this move are imprecise, and the distinction matters if you are ever asked to justify the work.
3:1 is two different requirements. At AA, it is 1.4.11 Non-text Contrast: 3:1 against adjacent colours. At AAA, it is 2.4.13 Focus Appearance, which asks for 3:1 “between the same pixels in the focused and unfocused states”, plus an area at least as large as a 2 CSS pixel thick perimeter. A ring can pass one and fail the other, so say which you mean.
The target-size floor is 24px, not 44px. WCAG 2.2 added 2.5.8 Target Size (Minimum), Level AA: “The size of the target for pointer inputs is at least 24 by 24 CSS pixels”, with five exceptions. The 44px figure is real but it is 2.5.5 Target Size (Enhanced), Level AAA, from WCAG 2.1: “The target for pointer input is at least 44 by 44 CSS pixels in size.”
Apple’s guidance lands on the same larger number for its own reasons: “As a general rule, a button needs a hit region of at least 44x44 pt”, and 60x60 pt in visionOS.
So 24 is the compliance floor and 44 is the usability target. Both are true, they are different claims, and conflating them is how a reasonable request gets dismissed by someone who has read the spec.
And WCAG 2.2 added one more thing you have probably broken. 2.4.11 Focus Not Obscured (Minimum), Level AA: “When a user interface component receives keyboard focus, the component is not entirely hidden due to author-created content.” If you have a sticky header, tab through your own page and watch what it hides.
The move
Never delete the focus ring, replace it. Never build a control out of a
div, use the element that already works.
/* BEFORE */
:focus { outline: none; } /* F78. fails 1.4.11, 2.4.7, 2.4.13 */
/* AFTER */
:focus { outline: none; } /* fine, because of the next rule */
:focus-visible {
outline: 2px solid var(--focus); /* >= 2px perimeter */
outline-offset: 2px; /* so it reads against the control */
}
:focus-visible is the piece that makes this painless. It applies the ring when the browser
judges it should be shown, which is keyboard navigation, and not when you click a button with
a mouse. Which was the actual complaint that made everyone reach for outline: none in the
first place.
What it costs
outline-offset and rounded corners fight each other, and on a tightly packed toolbar an
offset ring will overlap its neighbours. That is a real layout problem and the honest fix is
spacing, which is the previous part of this book, not a thinner ring.
A 44px target is a lot of pixels on a dense screen. In a data table with forty rows, 44px row actions will not fit. The compliance floor is 24px, and the exceptions in 2.5.8 exist precisely for this case, so read them rather than assuming you must choose between density and conformance.
And the div habit is usually a framework habit, not a preference. Component libraries
ship clickable divs, and replacing them is a bigger job than replacing your own. Start with
the ones you wrote.
Try this week
Put your cursor in the address bar of your own product and press Tab, repeatedly, all the way through one page. Do not touch the mouse.
Three things to watch for. Whether you can see where you are at all. Whether anything focused
disappears under a sticky header. And whether Tab ever skips something you can click, because
that is a div and it is invisible to every keyboard user you have.
Then fix the first one you find. If it is outline: none, the replacement is four lines and
you already have them above.