Focus, and where it goes

Keep the focus visible, and put it where the user’s attention should be.

07of 16
≈ 12Min read
4Parts

The indicator is not optional

Without it, a keyboard user has no idea where they are. If the default ring is too ugly for the design, replace it — do not remove it. WCAG 2.2 added a criterion about how visible it has to be: at least a 2px-thick line around the control, at 3:1 contrast against what is behind it.

the line that breaks a site for keyboard users
*:focus { outline: none; }        /* every year, in every codebase */
a replacement that is better than the default
:focus-visible {
  outline: 2px solid #00eadc;
  outline-offset: 2px;
  border-radius: 4px;
}

/* and for a dark control on a light page, the double-ring trick */
:focus-visible { outline: 2px solid #000; box-shadow: 0 0 0 4px #fff; }

:focus-visible is the answer to "it looks wrong on click"

The reason people remove outlines is that they appear when a mouse user clicks a button. :focus-visible is the browser's own judgement of whether the focus came from a keyboard: style that, and a mouse click stays clean while Tab shows the ring.

Keep a :focus fallback only if you support very old browsers; every current one implements :focus-visible.

Do not forget :focus-within

For a composite control — a search box with a button inside a bordered container — :focus-within on the container is what makes the whole thing look focused.

Moving focus on purpose

Focus is the screen reader's "you are here". When the page changes under somebody — a route change in a single-page app, a panel opening, a step completing — focus has to follow, or they are still reading the old thing while the screen shows a new one.

  • After a route change — focus the new h1, or a wrapper with the page name.
  • When a dialog opens — focus the dialog, or the first control in it. On close, focus what opened it.
  • After deleting a row — focus the next row, or the list itself. Never leave focus on a removed element: it falls back to <body> and the user is at the top of the page.
  • After a validation failure — focus the first field with an error.
  • Never on page load. Focusing a search box on load moves a screen reader past the heading and the navigation.
the pattern for a client-side route change
function navigate(url, title) {
  render(url);
  document.title = `${title} — DOMVYBE`;

  const heading = document.querySelector("main h1");
  heading.setAttribute("tabindex", "-1");     // focusable from script, not in the tab order
  heading.focus();                            // announced, and Tab continues from here
}

Do not steal it

Focus belongs to the user. Do not move it while they are typing, do not pull it into a newly-loaded advert, do not re-focus a field on every keystroke — a "helpful" focus() inside an input handler is why some forms are impossible to use with a screen reader.

Practice