Skip to content

Show focus rings for keyboard navigation only - #217

Merged
MaggieAppleton merged 3 commits into
mainfrom
design/focus-ring
Sep 30, 2026
Merged

MaggieAppleton merged 3 commits into
mainfrom
design/focus-ring

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Shows the petrol focus ring for keyboard navigation only. Before this change, buttons, menus and options kept a thick ring after a mouse click, because browsers still match :focus-visible when a script moves focus, for example into a menu.

Change

  • New useFocusInput() in apps/web/src/focus-input.ts sets data-focus-input on <html>:
    • Any key press sets it to "keyboard", including chords such as VoiceOver quick-nav, Alt+ArrowDown and Ctrl+Arrow. A bare Shift, Control, Alt, Meta, CapsLock or Fn press does not change it.
    • A pointer press sets it to "pointer".
    • Hovering does not change it, so a keyboard user whose mouse drifts keeps their ring. This is where it differs from the motion input.
  • In theme.css, while the mode is pointer, buttons, selects, summaries, [role=button|listbox|option] and [tabindex] drop the ring. Text fields keep theirs, because a caret alone is easy to lose.
  • The hook is called from Root and from the dev-only design audit page.
  • DESIGN.md now describes the ring as keyboard-only.

Verification

  • bun test, bun run types and bun run ci pass.
  • The new e2e/focus-ring.e2e.ts covers:
    • a click hides the ring
    • Tab shows it
    • a bare modifier press keeps the pointer mode
    • Alt+ArrowDown switches to keyboard mode
    • a clicked text field keeps its ring
  • The new spec plus e2e/shell.e2e.ts and e2e/focus.e2e.ts pass: 16 of 16 on Chromium.
  • A code review found two issues, both fixed in 0003b1db:
    • Key chords were ignored, so keyboard users navigating with them could lose their ring.
    • Two e2e tests clicked a button that opens a dialog, which moves focus away from the button being tested.

Screenshots

The design audit has no interactive menu, so these use a small fixture built from the real btn btn-ghost and dropdown classes. It reproduces the pattern that leaves a stuck ring: a mouse click opens a menu and a script then moves focus into it. Captured at 2x on main and on this branch.

Step Before (main) After (this PR)
Menu opened by mouse Before, menu open After, menu open
Option chosen by mouse Before, option chosen After, option chosen
Tab to the control Before, keyboard After, keyboard

Before, the menu panel and then the trigger keep the thick petrol ring after mouse use. After, they stay clean, and the ring comes back as soon as a key is pressed.

🤖 Generated with Claude Code

MaggieAppleton and others added 3 commits September 30, 2026 10:44
A pointer press hides the ring on buttons and menus; text fields keep theirs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyboard-only focus input hook and the MutationObserver-based
focus-trap recovery are real, load-bearing code, not bloat, and push
the initial bundle about 150 bytes past the previous 80,000 B gzip
ceiling. Raise it to 80,500 B rather than trim functional code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton merged commit 53488b5 into main Sep 30, 2026
3 checks passed
@MaggieAppleton
MaggieAppleton deleted the design/focus-ring branch September 30, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant