BXUP Track progress & test yourself — free
Frontend & Mobile · Playbook · Core · ~6 min read
FRONTEND & MOBILE · PLAYBOOK · CORE

Keyboard and Focus Repair Against APG Patterns

A custom dropdown that ignores Arrow keys, a modal that leaks focus to the page behind it, a tab bar operable only by mouse — keyboard bugs are the most common serious accessibility failures in custom widgets, and the ARIA Authoring Practices Guide already specifies exactly how each widget should behave. Repair against the spec, verify with a screen reader.

Tier: Core · Workflow area: Accessibility

This playbook uses AI to implement APG-conformant keyboard behavior — roving tabindex, focus traps, ARIA states — while keeping the acceptance test firmly human: hands on keys, screen reader talking.

When to use

  • An audit (pb-29) or bug report surfaced keyboard traps, unreachable controls, or dead arrow keys in custom widgets.
  • You are building (or an AI generated) a custom menu, dialog, combobox, tabs, tree, or listbox instead of using a primitive.
  • Focus behavior around async UI — spinners replacing buttons, content swaps, route changes — loses keyboard users' place.
  • A design-system primitive needs its interaction contract brought up to APG before it scales its defects everywhere.

Skip it when a maintained accessible primitive exists for the widget (Radix, React Aria, your design system) — adopting it is almost always the better repair than reimplementing the pattern.

Prerequisites

  • The ARIA APG pattern page for each widget you are repairing, open and read. It is the spec; the model is the implementer.
  • A keyboard-only session you can actually run: know Tab/Shift-Tab, Arrows, Home/End, Enter/Space, Escape, and your screen reader's basic navigation (NVDA or VoiceOver at minimum).
  • Component tests runnable locally (RTL/Vitest or platform equivalent) so keyboard behavior gets pinned once fixed.
  • Agreement on the repair order: adopt a primitive > extend a primitive > implement the APG pattern by hand. AI assists the third path; it does not justify choosing it.

The workflow

  1. Name the pattern and diff against its contract. For each broken widget, identify the APG pattern it should be (menu button? combobox? tabs with automatic or manual activation?). Then have the assistant produce the conformance diff:

    Here is our TabBar component source. Against the APG "Tabs" pattern:
    1. List every keyboard interaction the pattern requires (Tab, Arrow
       keys, Home/End, activation model) and whether our code implements it.
    2. List every required role/aria-* attribute (tablist, tab, tabpanel,
       aria-selected, aria-controls) and whether ours is present and correct.
    3. List focus-management requirements (roving tabindex vs
       aria-activedescendant) and what we currently do.
    Output a table: requirement | APG says | our code does | gap.
    

    Verify the "APG says" column against the actual APG page — models paraphrase specs with small, load-bearing errors.

  2. Implement the mechanics with AI, one concern per pass. Work the gap table in this order, as separate edits: (a) roles and states, (b) the keyboard event model, (c) focus management. For roving tabindex, be explicit:

    Implement roving tabindex for this toolbar per APG: exactly one child
    has tabindex="0" at any time, the rest tabindex="-1"; Arrow keys move
    both focus and the tabindex; focus re-entry returns to the last-active
    item; Home/End jump to first/last. Do not add aria-activedescendant —
    we are using the roving pattern.
    

    For dialogs: focus moves into the dialog on open (to the dialog or its first sensible control), Tab cycles inside it, Escape closes, and focus returns to the invoking element on close. Insist on the return-focus half — generated traps routinely forget it.

  3. Wire the states the pattern requires, not decorative ARIA. aria-expanded on the trigger, aria-selected on tabs, aria-checked on menuitemcheckbox, aria-current where applicable — driven by the same state as the visual change, never a parallel copy. Have the assistant sweep for contradictions: ARIA claiming one state while the rendered UI shows another is worse than no ARIA.

  4. Verify by hand, then by ear. Keyboard pass first: operate the widget completely without the mouse, checking every row of the gap table. Then the screen-reader pass: with NVDA or VoiceOver, confirm role and state are announced ("tab, selected, 2 of 4"), that arrowing produces sensible speech, and that opening/closing announces correctly. The model's claim of conformance counts for nothing here — only what you hear.

  5. Pin the behavior in tests. Convert the gap table into RTL tests using userEvent.keyboard and accessible queries: arrow-key traversal, Home/End, trap-and-return for dialogs, and toHaveAttribute("aria-selected", "true") assertions. These tests are what stop the next refactor — human or AI — from silently regressing the pattern.

Key Takeaway:

The APG is the contract, AI is the implementer, and your hands and ears are the acceptance test. A widget is fixed when the gap table is empty in the running app — not when the diff looks conformant.

Tool variants

  • Claude / Copilot in the IDE: best for the gap-table and mechanical-implementation passes with the component source in context.
  • Radix / React Aria / your design system: the "repair by replacement" path — audit still applies, but the pattern mechanics come pre-built and tested.
  • NVDA (Windows) / VoiceOver (macOS, iOS) / TalkBack (Android): the verification instruments. Test with at least the one your users most use; NVDA+Chrome and VoiceOver+Safari behave differently enough that "works in one" is not "works."
  • Mobile note: the same discipline applies to Compose and SwiftUI — focus order, Modifier.semantics/accessibility traits, and TalkBack/VoiceOver traversal replace tabindex mechanics.

Verification

  • The widget's gap table shows every APG requirement implemented, verified live — not inferred from source.
  • Full keyboard operation succeeds with visible focus at every step, no traps, and focus return on every dismiss.
  • Screen-reader run announces role, name, and state correctly at each interaction; a short recording or notes are attached to the PR.
  • Keyboard-behavior tests exist, pass, and fail when you deliberately break the handler (spot-check one mutation by hand).

Pitfalls

  • ARIA as paint. Adding role="menu" without the keyboard model makes things worse: the screen reader promises menu behavior the widget cannot deliver. Roles are contracts; only claim ones you implement.
  • Trusting generated focus code. Models produce plausible traps that miss edge cases — Shift-Tab at the first element, focus return after the trigger unmounted, portals breaking Tab order. Every one of these is a manual test row.
  • Mixing roving tabindex with aria-activedescendant. Pick one focus strategy per APG guidance for the widget and say so in the prompt, or the model will blend both.
  • Testing only with Tab. Most composite-widget behavior lives on Arrow keys, Home/End, and typeahead. A Tab-only pass validates almost nothing about a listbox or menu.
  • Fixing instances, not the primitive. If three screens have the same broken dropdown, the repair belongs in the shared component — or in adopting a library primitive — once.

Restricted-environment variant

Keyboard and focus repair is fully local work: the APG is public documentation, and screen readers run on your machine.

  • In-tenant or local models: the gap-table and implementation prompts work with any code-capable model; with smaller local models, paste the relevant APG keyboard-interaction section into the prompt rather than relying on trained knowledge of it.
  • No model at all: the APG pages include working reference implementations — the human-only path is reading the pattern and porting its example, which remains the gold standard.
Warning

Do not let an agent "verify" screen-reader behavior by reading the DOM. Accessibility tree inspection is a useful proxy during development, but real AT has real quirks — acceptance is a human with NVDA or VoiceOver, every time.

Success criteria

  • Every repaired widget is fully operable by keyboard alone and announces correctly in at least one real screen reader.
  • Focus is never lost: async swaps, dismissals, and route changes all land focus somewhere sensible and visible.
  • Pattern-conformance tests are in CI, and the widget's next refactor passes them without accessibility review finding regressions.
  • The team defaults to primitives for new widgets — this playbook's usage trends toward legacy repair, not new-code rescue.
This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in