From armor
Verifies WCAG 2.1.1 (Keyboard) compliance by testing fuzzy, property-based focus invariants against a live page. Use when auditing keyboard accessibility, testing focus management, validating tab order, or checking that interactive elements are reachable.
npx claudepluginhub markacianfrani/armor --plugin armorThis skill is limited to using the following tools:
A fuzzy, property-based approach to keyboard accessibility testing. Rather than
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
A fuzzy, property-based approach to keyboard accessibility testing. Rather than asserting specific widget behavior, this skill verifies broad invariants that a well-behaved UI should satisfy. These catch classes of bugs — missing tabindex, focus traps, incorrect tab order, zombie focusable elements, roving tabindex bugs — without hard-coding element names.
At its simplest, the keyboard focus checks are:
For whole documents, these checks are boundary-aware: browser chrome, body,
iframe edges, and document wrap points may end traversal. For intentional traps
such as modals, dialogs, menus, and popovers, the stricter cyclic version applies.
Traditional UI tests say "when I click X, Y happens." These tests say "given any
page with focusable elements, these properties should hold." The properties must
match the platform: a normal document is not a closed focus trap. Browser tab
navigation can legitimately leave page-owned focus and land on body, browser
chrome, or another boundary. Treat that as a boundary, not automatically a bug.
Use stricter cyclic assertions only for contexts that promise a trap, such as modal dialogs.
This skill is intentionally loose. The magic is in using invariants as probes, not as a brittle spec for how every page must behave. When a property appears to fail, classify what happened before calling it a bug.
Ask:
body, browser chrome, no deep
active element, iframe boundaries, and document wrap points are often normal.Report surprising-but-valid behavior as observations or boundaries. Fail only when the behavior violates a user-centered keyboard expectation: unreachable controls, stuck focus, invisible focus, focus escaping an active modal, focus entering hidden/off-screen UI, or an early loop that prevents reaching available controls.
Complementary to code coverage: invariants exercise the happy geometry. If coverage shows an uncovered branch, that's where a specific test is needed.
The page under test must already be loaded.
Preferred: use Chrome MCP/devtools tools:
chrome_devtools_take_snapshot to inspect the pagechrome_devtools_evaluate_script to enumerate focusables and query active
elementchrome_devtools_press_key for Tab / Shift+TabOnly use raw CDP if the devtools tools are unavailable. In that case, use CDP to send Tab/Shift+Tab and query the page's deep active element.
body, no
deep active element, browser chrome, or an implementation-specific wrap point.focus:existenceIf there is one or more page focusable elements, pressing Tab from the body reaches a page focusable element or a documented initial focus target.
Procedure:
<body>: document.body.focus()<body> and is either:
What it catches: Pages where no element is keyboard-reachable. Missing
tabindex, all elements with tabindex=-1, JavaScript not mounted, or an
incomplete enumerator.
focus:progressionFrom any page focusable element, Tab should either move to a different page focusable element or cross a boundary. It should not remain stuck on the same element unless an intentional trap explicitly handles that case.
Procedure:
page-focusable, boundary, or same
d. Fail on same unless E is inside an intentional trap with expected
behaviorWhat it catches: Accidental focus traps, elements that consume Tab without advancing focus, broken roving tabindex implementations.
focus:adjacent-reversibilityFor observed adjacent page-owned transitions, reverse traversal should undo forward traversal.
This replaces the overly prescriptive global round-trip invariant. A normal
web page is not required to have a closed cyclic tab order.
Procedure:
<body> and press Tab until you have observed page-owned transitions
or hit a boundary/loop/safety limit.A --Tab--> B where both A and B are page
focusables:
a. Focus B
b. Press Shift+Tab
c. Assert focus returns to AWhat it catches: Asymmetric local tab order, elements present in forward order but skipped in reverse, dynamic insertion/removal that breaks adjacent navigation.
focus:no-early-loopTraversal should not revisit a page focusable before visiting all currently reachable page focusables, unless it first crosses a boundary or is inside an intentional trap.
Procedure:
<body>, press Tab.N + 5 tabs.What it catches: Loops inside part of the page, duplicate/repeated tab stops, incorrect tabindex ordering that prevents later controls from being reached.
focus:trap-cycle — modal/dialog onlyWhen an intentional focus trap is active, Tab and Shift+Tab cycle only within the trap and round-trip behavior is expected.
Procedure:
What it catches: Leaky modals, inaccessible dialogs, broken first/last sentinels.
Use a fuzzy page-owned focusable set:
tabindexsummary, details, and contenteditable elementsIf keyboard traversal reaches a real target the enumerator missed, treat that as an enumerator issue before treating it as a UI bug.
Use a deep active element check so focus inside open shadow roots is attributed to the actual focused element, not just the shadow host.
Keep the report short and invariant-centered:
focus:existence: pass/fail/skip, with the first focused targetfocus:progression: pass/fail/skip, with any element that gets stuckfocus:adjacent-reversibility: pass/fail/skip, with the transition that does
not reversefocus:no-early-loop: pass/fail/skip, with any repeated element before all
reachable elements are visitedfocus:trap-cycle: pass/fail/skip, only when a modal/dialog trap is activeFor failures, include enough context to reproduce: which element, key sequence, what was expected vs. observed, and whether a boundary was crossed.
Report boundary crossings as observations, not failures.
Additional invariants to consider:
Each can be expressed as a fuzzy invariant with boundary-aware classification.