Skip to content

a11y: make 2D keycap grid keyboard/screen-reader operable - #432

Open
fla-rion wants to merge 2 commits into
the-via:mainfrom
fla-rion:a11y/keycap-2d-grid
Open

a11y: make 2D keycap grid keyboard/screen-reader operable#432
fla-rion wants to merge 2 commits into
the-via:mainfrom
fla-rion:a11y/keycap-2d-grid

Conversation

@fla-rion

Copy link
Copy Markdown

Fixes #431 (first step, scoped to the 2D keycap grid).

What

  • Every interactive keycap in 2D mode (unit-key/keycap.tsx) now gets role="button", tabIndex, aria-pressed, and an aria-label built from the same label data that's already used to paint the canvas legend.
  • Enter/Space on a focused keycap now triggers the same selection path as a click.
  • The global *:focus { outline: none } reset is replaced with a :focus-visible-based one, so keyboard focus is visible again without adding an outline on mouse clicks.

Why

VIA configures keyboards built specifically for open, hackable firmware, but the remapping UI can't be operated by a screen reader today: keys are unlabeled, unfocusable divs, and the visible legend is drawn to a <canvas> (pixels, not text) — see #431 for the full breakdown of both render modes.

Scope / follow-ups

This PR only touches the primary keycap path in 2D mode, since it's already plain DOM and the smallest correct increment. Not yet covered (tracked as follow-ups):

  • ComboKeycap / EncoderKey variants
  • Arrow-key roving-tabindex navigation across the key grid (Tab order already works via the new tabIndex, just not spatial arrow nav yet)
  • 3D render mode (would need a parallel, non-visual interaction path since it's WebGL-only)
  • The base Button component (inputs/button.tsx) is a styled.div app-wide, not a native <button> — separate, bigger change

Testing

  • npx tsc --noEmit passes with no new errors.
  • Manually verified via VoiceOver-style code review (no dev HID device available in this environment) that the new aria-label/role/tabIndex/onKeyDown wiring reuses existing, already-computed label/selection state rather than introducing new state.

fla-rion added 2 commits July 27, 2026 20:25
Each keycap in the 2D render mode was a plain div with only mouse
handlers - no role, no tabIndex, no aria-label (the visible legend is
canvas-painted pixels, not text), and no way to activate it from a
keyboard or screen reader.

- Give interactive keycaps role="button", tabIndex, aria-label sourced
  from the same label data used for the canvas paint, and aria-pressed
  for the selection state.
- Handle Enter/Space via onKeyDown to activate the same selection path
  as a click.
- Replace the blanket `*:focus { outline: none }` with a
  `:focus-visible`-based reset so keyboard focus is visible again
  without affecting mouse interaction.

Scoped to the primary keycap render path for this first pass; the
ComboKeycap/EncoderKey variants and arrow-key grid navigation are
tracked as follow-ups in via-app#431.
- KeyGroup now tracks a roving tabindex across the 2D key grid and
  handles ArrowUp/Down/Left/Right by finding the nearest key in that
  direction (weighted towards staying in the same row/column, since
  the physical layout isn't a strict grid), keeping only one keycap in
  the Tab order at a time and moving DOM focus programmatically.
- Extends the role/tabIndex/aria-label/onKeyDown treatment from the
  previous commit to the ComboKeycap and EncoderKey variants, which
  were left out of the first pass.

Still open (see via-app#431): 3D render mode, and the app-wide
Button component being a styled.div instead of a native <button>.
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.

Keyboard visualization and controls are inaccessible to screen reader users (no ARIA, no keyboard operability, canvas-only labels)

1 participant