R3.10.2focus-visibledesign

Focus visibility must not depend on whether a mouse is in use

Aliases: input modality · :focus · keyboard focus ring · session mouse flag

What it is

Whether a focus indicator is drawn must follow this focus event's input modality, not a session-level verdict such as "this person has used a mouse". A click moves focus onto a button, which tempts a global :focus { outline: none }; later, when the same person Tabs, uses a switch, or speaks a command, nothing marks which control is active. :focus-visible binds "should a ring be painted" to modality: keyboard-equivalent focus paints; a pointer click may not — the next Tab must still paint.

This is the condition for showing the indicator, not whether the stroke contrasts on every background, and not whether a replacement exists after the UA outline is removed.

Why it happens

Focus is the document's active element. It does not remember the last pointing device. Clicks, touches, Tab, arrow keys, and screen-reader gestures all change document.activeElement. If visibility is implemented as "on mousemove, stamp a class on html and hide every outline thereafter", modality freezes into a session flag. Putting the mouse down and picking up the keyboard does not reliably flip the flag, and even if it does, the keyboard stops in between already had no mark.

:focus-visible heuristics follow a single focus event: match when the event is keyboard or keyboard-equivalent (including some assistive tech); often do not match when it is a pointer click. It is not an identity label for "keyboard users". One person can click and then Tab; those two stops must be allowed different visibility. Painting :focus-within on a container, or deepening fill on hover and calling it focus, collapses pointer state and keyboard state. Hover highlight cannot tell you where focus is now.

Where it stops holding

Always-on rings are noisy on touch-first screens, but "noisy" does not imply "hide forever after a touch was seen". Forced-colors / always-show-focus settings require the ring to stay; :focus-visible must not override them. Whether scripted element.focus() matches :focus-visible varies by engine — after programmatic focus into an overlay, do not assume the ring will or will not appear; a selected appearance that does not depend on the pseudo-class may be needed. Contrast and thickness are a separate layer: the condition can be right and the ring still dissolve into the background. IMEs, gamepads, and switch controls have their own modalities; testing only mouse-versus-keyboard is not enough.

Applying it

  • When removing the default outline, decide when to redraw with :focus-visible (or equivalent per-event modality detection). Never hide all focus marks because "this session used a mouse / touch".
  • Do not substitute hover fill or a :focus-within container stroke for the control's own indicator.
  • After programmatic focus(), check whether this engine paints a visible indicator; if not, give the newly focused control a selected look that does not depend on the pseudo-class.
  • How to check: mouse-only through the main path — click states may omit the ring. Without reloading, Tab the same path; every focusable control must show an indicator. Then turn on a system high-contrast or always-show-focus setting and confirm site CSS has not eaten the ring.

Related

  • Same group: R3.10.1 Focus inside an overlay must stay within its interactive range · R3.10.3 When a focused element is removed, a successor must take focus
  • Nearby: J2.06 Focus Visibility · R3.02 Focus Management
  • Search terms: focus-visible · input modality · focus indicator · :focus

Cards in the same group

Quick Actions

Share

Share this page

ios_share

https://hci.top/en/handbook/R3.10.2