--- name: verify-keyboard-access description: Verify that every interactive control on a page can take keyboard focus and that its primary action fires from the keyboard rather than only the mouse, and say plainly that Tab order cannot be verified today. Use when a component was hand-tested with a mouse, a custom dropdown/modal/combobox was added, or an accessibility report flags an unreachable control. license: Apache-2.0 metadata: version: 3.7.0 homepage: https://www.reticle.sh repository: https://github.com/reticlehq/reticle --- # Reachable is not the same as operable A `
` styled to look like a button can look reachable (plenty of component libraries hand out `tabindex="0"` for free) and still do nothing when you press Enter on it. Being able to take focus proves a control is reachable, not that it works. **Reticle** can drive both halves in the running app. Not installed? `RETICLE_INSTALL_SOURCE=npx_skill npx @reticlehq/server@latest init`, then the [`install-and-verify`](https://github.com/reticlehq/reticle/blob/main/skills/install-and-verify/SKILL.md) skill. ## Read this before you start: Tab order cannot be verified today `press` always goes through a synthetic key event, and a synthetic Tab does not move focus. That is true in every mode, including `reticle drive`: there is no mode in which an agent can prove focus order. Do not go looking for one, and do not report a Tab-order result as passed or failed. Report it as **unknown**. What this skill can prove is narrower: that each control can take focus, and that its primary action fires from the keyboard. ## Confirm the session is healthy Confirm a live session exists for the page and can render and observe: ``` reticle_session({ action: "list" }) ``` In a disconnected, hidden or throttled tab, a key press can be accepted while timers, rendering and later observation do not advance, so nothing you read back is trustworthy. Get a usable context first, for example a leased tab: ``` reticle_run({ tool: "reticle_lease", args: { action: "acquire", url } }) ``` Pass `refuseWhenThrottled: true` on the action so a paused tab fails loudly instead of silently doing nothing. If you cannot get a healthy session, report the run as **blocked**, not passed, and do not keep retrying against it. ## List the controls List every interactive control before touching the keyboard, or you will only test the ones you already knew about: ``` reticle_look({ sessionId, action: "page", mode: "interactive" }) ``` ## Check that each control can take focus For each control from that list, focus it and assert that it took focus: ``` reticle_act({ sessionId, ref, action: "focus" }) reticle_assert({ sessionId, predicate: { kind: "element", query: { testid: "the-control" }, state: "focused" }, }) ``` That proves the control can take focus. It does not prove the control sits in the Tab order (`tabindex="-1"` is focusable but skipped by Tab), so report it as focusable, not Tab-reachable. A `
` with no `tabindex` will not take focus, and that is a real finding. ## Prove the primary action, not just the focus Focus is a precondition, not the check. Name the consequence, then activate with the keyboard: ``` reticle_act_and_wait({ sessionId, ref, action: "press", args: { text: "Enter" }, until: { kind: "element", query: { testid: "expected-result" } }, }) ``` Buttons and links take Enter. Checkboxes and native `