Reader and browser support
These combinations describe the available integrations. A detected browser version confirms installation; it does not prove that a website works with that reader. Run a reader smoke check against the build being assessed and keep its transcript.
| Reader and host | Browser | Available checks and limits |
|---|---|---|
| VoiceOver on macOS | Chrome, Edge, Brave, Chromium | Guidepup controls the real reader with observed target checks. Playwright supports separate DOM scans in Chromium browsers. Correlate scans and reader observations with the tested page and state. |
| VoiceOver on macOS | Safari | Select Safari with --browser Safari. The current scan integration uses Chromium; a scan in another browser does not prove Safari behavior. |
| NVDA on Windows | Chrome, Edge, Brave, Chromium | Commands check the observed foreground target before controlling the installed NVDA bundle. DOM scans use Chromium. Acceptance still requires a Windows transcript; macOS and simulated tests cannot verify this pairing. |
| Virtual reader on any supported host | Headless Chromium or jsdom | Simulation for development. jsdom does not run page scripts. Neither engine provides real assistive technology evidence. |
| VoiceOver on Windows or Linux; NVDA on macOS or Linux | Any | Unsupported: these readers require their native operating system. |
| Other readers or browser combinations | Including Firefox | No verified audit integration. Opening an application by name does not establish support for a complete assessment. |
Start a session
A session is a running screen reader that later a1 sr commands control.
sr start takes the page URL as its argument and the screen reader as
--sr: voiceover, nvda, or virtual.
For VoiceOver or NVDA, a11ied opens the URL in a browser, waits until that window is in
front, after starting the screen reader under its desktop ownership lease. --browser chrome, edge,
brave, chromium, or an app name such as Safari
picks the browser. Without it, a11ied uses the system automation browser.
Native sessions default to guarded. Commands check the observed
foreground target, and typing checks between characters. Read the fresh
current item, focus observations, and transcript after each action. If focus
changes, refocus deliberately and inspect state before continuing.
require-binding is an optional strict mode that refuses unavailable
native binding. development bypasses target checks. Foreground
checks do not guarantee that focus cannot change during dispatch.
a1 sr start --sr voiceover http://localhost:3000/checkout --json
a1 sr start --sr voiceover --browser Safari http://localhost:3000/checkout --app <name> reads a native app that is already open instead of a
page. The session brings the app to the front by name and waits for it to get there.
a1 sr start --sr voiceover --app Calendar --sr virtual starts the simulated screen reader: a screen reader implemented
in software that runs inside a headless Chromium page and produces the same kind of
announcements as VoiceOver or NVDA, with neither installed.
a1 sr start --sr virtual http://localhost:3000/dialog.html Session ready
Target: virtual
Engine: browser (a headless Chromium page; the page scripts run)
URL: http://localhost:3000/dialog.html
Recording: none
Idle timeout: 30 min
Every sr command now uses this session. Stop it with: a1 sr stop Target: is the screen reader that started. Engine: is
browser when the page runs in headless Chromium, so a live region, a part
of the page marked to announce its own content changes, is heard when a script fills
it, and a dialog is heard when a click opens it. When no Chrome, Edge, Brave, or
Chromium is installed, the engine is jsdom, an in-memory document where page
scripts do not run, and only what is in the markup is announced. See
screen readers for the details of both engines.
Commands use the active session. A second sr start stops that session
and reports its replacement. To retry cleanup for a retained recovery owner,
use a1 sr stop --session-id <id>. Failed browser focus confirmation
returns browser-focus-unconfirmed and cancels startup.
In library code, only one in-process JSDOM session can own the document.
Starting another returns virtual-session-conflict without changing
the existing reader. Stop that reader or use a separate process or browser engine.
Move and read
A screen reader moves through a page one element at a time: a heading, a link, a button,
a form field, a paragraph, a landmark. The element under its cursor is the current
element. Each time the cursor moves, the screen reader announces the element it reached.
The CLI prints that announcement as Phrase:, and the element's text as
Item:.
a1 sr next
a1 sr next heading sr next virtual
Phrase: main
Item: none
Role: main
sr next heading virtual
Phrase: heading, Dialog demo, level 1
Item: Dialog demo
Role: heading level 1
Name: Dialog demo
The first command moved the cursor onto the main landmark, which has no
text of its own. The second jumped to the next heading and printed its level and name.
sr next, sr previoussr top, sr bottomsr interact, sr stop-interacting, sr activatesr escape Press Escape, which closes a menu or a dialog. sr read Print the current element without moving: role, name, value, states, the announcement, and which of those came from the page and which from the announcement. sr title Print the page title: document.title on the simulated screen reader, the window summary (VO-F2) on VoiceOver, the window title (NVDA-T) on NVDA. sr find "Learn more" Move the cursor to the next element whose text contains the argument. Exits 4 when no element does. Needs an active session. sr press Tab Tab Press key chords in order, one chord per argument. sr type "test@example.com" Type into the focused form field. sr open <url> Load another page in the running session. a1 sr read sr read virtual
Phrase: heading, Dialog demo, level 1
Item: Dialog demo
Role: heading level 1
Name: Dialog demo
Source: virtual: role and name from the active node (dom-accessibility-api), states from its attributes Source: says where the role, name, and states came from. The simulated
screen reader reads them from the DOM node under its cursor. VoiceOver reports the
element's text and an announcement in the form name, role, so the role and
states are parsed from the announcement and the name is the element's text. NVDA reports
only the announcement, so all three are parsed from it.
With --json, every response has a state object with
lastSpokenPhrase (the announcement), currentItemText (the
element's text), currentItem (the role, name, value, states, and heading
level, plus a source field), the running spokenPhraseLog (every
announcement so far), and the timestamped transcript.
state.observations reports availability separately for keyboard
focus, reader-cursor identity, and target identity. An unavailable observation
includes a reason and, for query failures, an error code. Keyboard-focus
properties can differ from the reader's current item. Native reader-cursor
identity is unsupported by the installed reader connection; speech text alone
cannot prove which native element or document holds that cursor.
Add --phrase to print the announcement alone, one line, for shell loops. Add
--timeout <ms> to bound how long the screen reader command may take.
Navigate by heading, link, or landmark
Screen reader users do not read pages one element at a time. They jump by heading,
link, or landmark: a section of the page marked with a role such as
main, navigation, or banner, so a screen reader
can skip straight to it. sr next <kind> and
sr previous <kind> do the same on every screen reader. The kinds are
heading, link, landmark, control,
button, table, list, graphic,
region, and form-field.
a1 sr next heading
a1 sr next heading --level 2
a1 sr previous link
a1 sr next control --times 3 sr elements <kind> works like the VoiceOver rotor or the NVDA elements
list: it moves the cursor to the top of the page and prints every element of one kind
with its announcement. It stops at the end of the page or at --max, which
defaults to 200, and says which. elements, read-all,
goto, and find need an active session and exit with
missing-session without one. Run
sr start --sr virtual <url> first for the simulated
screen reader, since a plain sr start starts VoiceOver on macOS.
sr elements heading virtual
4 heading elements from the top of the page.
1. heading level 1: Structure page heading, Structure page, level 1
2. heading level 2: Products heading, Products, level 2
3. heading level 3: Pricing heading, Pricing, level 3
4. heading level 2: Sign up heading, Sign up, level 2
Stopped at the end of the document.
Each kind maps to Guidepup's jump method or a VoiceOver Commander command, to an NVDA
single-letter key, and to a virtual.commands.moveToNext* command. The
simulated screen reader has no command for button, control, table, list, graphic, and
form-field, so it moves element by element until the announced role matches, and reports
moved: false when the page has none.
sr goto --role button --name "Open dialog" moves the cursor forward until the
current element has that role, that name, or both, and exits 4 when nothing matches
before the end of the page.
sr goto --role button --name Open dialog virtual
Phrase: button, Open dialog
Item: Open dialog
Role: button
Name: Open dialog
Found: yes
Inside a table, sr table next-cell, previous-cell,
next-row, previous-row, next-column, and
previous-column move between cells. sr table column-header and
row-header print the headers of the current cell. VoiceOver moves with
VO-arrows inside an interacted table and reads the column header with VO-C. NVDA moves
with Control-Alt-arrows and speaks headers as the cursor enters a cell, so the two header
commands exit 2 on NVDA with that explanation.
Read the whole page
Two commands read a page. sr walk reads a whole one and sets up the
session for you. sr read-all reads from wherever the cursor already is,
in a session you already started.
sr read-all moves the cursor element by element from its current position to
the end of the document, bounded by --max (default 500), and prints the
transcript: every announcement in order, with a timestamp each. It says whether it stopped
at the end or at the cap.
sr walk [url] does all of this in one command: it starts a session when
none is active, or loads the URL in the active one, moves the cursor to the top, runs
read-all, and prints the transcript. With no session and no
--sr, walk starts VoiceOver on macOS. Pass
--sr virtual for the simulated screen reader instead.
a1 sr walk http://localhost:3000/dialog.html --sr virtual
a1 sr walk --max 40 --out ./transcripts/checkout-walk.md ! No session was active, so one was started.
# Transcript: virtual on http://localhost:3000/dialog.html
Started 2026-09-06T11:07:17.840Z. Exported 2026-09-06T11:07:18.044Z. 5 phrases.
1. [11:07:18.043] main
2. [11:07:18.043] heading, Dialog demo, level 1 (Dialog demo)
3. [11:07:18.043] button, Open dialog (Open dialog)
4. [11:07:18.043] end of main
5. [11:07:18.043] end of document
Stopped at the end of the document. Each numbered line is one element the cursor stopped at and what the screen reader said there. The text in parentheses is the element's text, when it has any.
Transcript assertions
sr expect searches the transcript for an announcement and exits 4 when
none matches. Plain text matches without regard to case.
/pattern/flags is a regular expression. --since <checkpoint>
searches only the announcements after that checkpoint, and --not passes
when nothing matches.
a1 sr expect "dialog, Preferences" --since opened
a1 sr expect /error/i --not sr expect
Expected: "dialog, Preferences"
Checked: 2 phrases since "opened"
Matched: dialog, Preferences, modal
Result: pass sr wait --for <text|/regex/> polls the transcript until an
announcement made after the wait began matches, and exits 4 at
--timeout (default 5000 ms). It reads the whole transcript rather than
the last announcement, so an announcement that arrives and is replaced before the
next poll still counts.
Use it for an announcement that arrives later than the command that caused it, such
as a live region a network response fills, or the delay before VoiceOver finishes
speaking. Use --since <checkpoint> to include announcements after a checkpoint,
including those that arrived before the wait command started. Without --for,
wait pauses for --ms.
a1 sr checkpoint before-save
a1 sr press Enter
a1 sr wait --for /saved/i --since before-save --timeout 3000 find, goto, wait --for, expect, and a
batch with a failed expect exit 4. Usage errors, including a command the
screen reader cannot run or a command run with no active session, exit 2. A browser
or broker process that fails to start exits 3.
Run a batch of actions
sr batch [file] reads one action per line, from the file or from stdin, and
runs them all over one connection to the session process. Each line is the JSON request
the MCP sr_action tool takes, or an expect line. Blank lines and
lines starting with # are skipped. The batch stops at the first failed expect
unless --continue is set.
a1 sr batch <<'EOF'
{"action":"goto","payload":{"role":"button","name":"Open dialog"}}
{"action":"checkpoint","payload":{"label":"opened"}}
{"action":"activate"}
{"action":"expect","payload":{"match":"Preferences","since":"opened"}}
{"action":"escape"}
{"action":"expect","payload":{"match":"Open dialog"}}
EOF sr batch
✓ 1. goto {"role":"button","name":"Open dialog","max":500} button, Open dialog
✓ 2. checkpoint {"label":"opened"} button, Open dialog
✓ 3. activate textbox, Nickname
✓ 4. expect Preferences since "opened" pass
✓ 5. escape button, Open dialog
✓ 6. expect Open dialog pass
6 steps passed.
The checkpoint runs before activate, not after, so expect --since
searches the dialog's announcement. An announcement already printed by the triggering
action happened before a later checkpoint and would not count.
Run named screen reader commands
sr do runs a command by name. The portable set works on every screen reader
and resolves through the same table the subcommands use: next,
previous, interact, stop-interacting,
activate, top, bottom, escape, and
next-<kind> and previous-<kind> for every kind above.
a1 sr do interact --json
a1 sr do next-heading --json
a1 sr do stop-interacting --json
VoiceOver and NVDA add their own sets. sr list prints them grouped by command
set and by what the commands do. Start with --query: the full list is over
400 lines.
a1 sr list --query heading
a1 sr list --query "VO-Command" --sr voiceover
When two sets share a name, prefix it with the set:
sr do voiceover-keycode:move-to-next or
sr do voiceover-commander:move-right. --command-set picks one
set for the whole call. The simulated screen reader accepts the portable set only.
macOS VoiceOver
VoiceOver Commander vs. Keycodes
When driving VoiceOver on macOS, a11ied supports two distinct command sets for controlling the screen reader:
VoiceOver Commander (voiceover-commander) and VoiceOver Keycodes (voiceover-keycode).
Choose a command set based on whether the test needs VoiceOver commands or simulated keystrokes.
What is VoiceOver Commander?
VoiceOver Commander is an internal, semantic automation interface built directly into macOS VoiceOver.
Instead of simulating physical keys, a11ied communicates directly with VoiceOver via Apple Events (AppleScript):
tell application "VoiceOver"
tell commander to perform command "move down"
end tell Because Commander interacts directly with VoiceOver's internal automation dictionary:
- Layout-independent: It is completely immune to physical or virtual keyboard layout differences (e.g., QWERTY, Dvorak, AZERTY, or international input sources).
- Modifier-independent: It does not require synthesizing or holding the VoiceOver modifier keys (
Control+Optionor Caps Lock) and is unaffected by the VO-Lock state (VO-;). - Fast and deterministic: Commands execute as direct AppleScript transactions without waiting for OS keyboard event queues, key-repeat delays, or modifier release transitions.
- Default choice: In
a11ied,--command-set autoresolves tovoiceover-commanderfor all VoiceOver actions because it provides maximum reliability.
What are VoiceOver Keycodes?
VoiceOver Keycodes control VoiceOver by simulating literal hardware key events through the macOS System Events API (or native CoreGraphics accessibility event taps).
When you issue a keycode command like move-to-next, a11ied instructs macOS to simulate pressing the physical VoiceOver chord:
tell application "System Events"
key code 124 using {control down, option down} -- Right Arrow with VO keys
end tell Keycodes replicate the physical user experience of pressing keys on an Apple keyboard:
- Physical fidelity: Simulates physical key-down, modifier hold, and key-up event cycles, identical to a human pressing
Control+Option+ArrowRight. - Keyboard layout dependent: Keycodes correspond to physical key positions (scan codes). Mismatches between hardware layouts or active OS input sources can affect command interpretation.
- Focus sensitive: Requires the target application window to be frontmost and actively receiving input focus. If focus shifts during key dispatch, keystrokes may be lost or misrouted.
- Timing considerations: Subject to OS event loop dispatch latency and modifier state synchronization.
Comparison & Contrast
The table compares how the two command sets execute commands.
VoiceOver.app; Keycodes send synthetic key events to macOS System Events. Control+Option. Using both in the CLI
By default, a1 sr do runs Commander commands on macOS. To run a keycode command, either qualify the command name or set --command-set voiceover-keycode:
# Run via VoiceOver Commander (default)
a1 sr do move-right
# Run explicitly via VoiceOver Keycodes
a1 sr do voiceover-keycode:move-to-next
# Select the keycode command set for the invocation
a1 sr do move-to-next --command-set voiceover-keycode
# Press literal key chords directly
a1 sr press "VO+ArrowRight"
Use a1 sr list --sr voiceover to explore the complete catalog of available commands across both sets.
Checkpoints and transcripts
A checkpoint is a named marker in the transcript. sr transcript --since and
sr expect --since then read only the announcements after it.
a1 sr checkpoint "opened the dialog"
a1 sr next
a1 sr transcript --since "opened the dialog"
Each transcript entry has an index, an ISO timestamp, the announcement, and the
element's text when the screen reader reported one. The Markdown output numbers the
announcements 1, 2, 3 and prints a checkpoint as a heading between them.
--tail <count> keeps the last N announcements.
Checkpoints between those announcements are also included. To cap all entries,
use --limit <count> (up to 1000). For index paging, use
--after-index <index>; -1 starts at the beginning.
Index paging defaults to 200 entries. Use transcript.window.nextAfterIndex
from the JSON output for the next page. hasMore reports whether
more selected entries remain. complete reports whether this response
includes every session entry. Use a cursor only with the session that returned it.
Selected responses omit duplicate raw speech logs and include only their checkpoints.
A transcript without selection options includes every retained entry.
--out <path> writes the transcript to a file, as JSON for a
.json path and as Markdown for a .md path. Pass
--format json or --format md to pick the format yourself.
a1 sr transcript --out ./transcripts/checkout.md Stream announcements in real time with sr tail
sr tail streams announcements continuously in real time as the screen reader speaks them,
similar to tail -f on a log file. It prints existing entries (default last 10, or --lines 0 to start fresh)
and then outputs every new spoken phrase as it arrives.
a1 sr tail
a1 sr tail --lines 0
a1 sr tail --since "opened the dialog"
a1 sr tail --json --phrase outputs just the spoken text without timestamp headers.
--json streams structured newline-delimited JSON (NDJSON) objects, one per line.
Aliases for tail are follow, stream, and watch.
On VoiceOver, sr screenshot <path> saves a picture of the element the
cursor is on. NVDA has no cursor screenshot and the simulated screen reader has no screen,
so both exit 2 and say so.
Example: a dialog
The page has one heading and a button named Open dialog. The button opens a modal dialog
named Preferences and moves focus to its first field. This transcript comes from
sr start, sr next, sr next heading,
sr goto --role button --name "Open dialog",
sr checkpoint opened, and sr activate, in that order, followed
by sr transcript.
# Transcript: virtual on http://localhost:3000/dialog.html
Started 2026-09-06T11:07:38.042Z. Exported 2026-09-06T11:07:44.590Z. 6 phrases.
1. [11:07:38.042] document
2. [11:07:38.876] main
3. [11:07:39.749] heading, Dialog demo, level 1 (Dialog demo)
4. [11:07:41.203] button, Open dialog (Open dialog)
## opened (11:07:41.880)
5. [11:07:42.555] dialog, Preferences, modal (Preferences)
6. [11:07:42.555] textbox, Nickname (Nickname)
Lines 1 to 4 are the elements the cursor stopped at on the way to the button. The
opened heading is the checkpoint, placed before activate. Lines
5 and 6 are what the screen reader announced when the button was pressed: the dialog,
with its name and its modal state, and then the text field that received
focus. Three things to check in a transcript like this. Every control has a name, so a
line that reads button alone is a defect. The order matches the visual
order. The states that matter are announced, which is why modal appears on
line 5.
For a validation message, submit the form with sr press Enter, then run
sr wait --for /error/i to find out whether the message was announced.
Status Messages is criterion 4.1.3, and its test method is
hybrid because the announcement is the part a person has to judge.
Stop the session
a1 sr stop --out ./transcripts/checkout.json
Stopping quits the screen reader process, finishes any recording, and deletes the
session state file. A session left running keeps VoiceOver or NVDA under automation
control, so stop each one you start. A session nobody has used for 30 minutes stops
itself. Pass --idle-timeout <minutes> on sr start to
change that, or --idle-timeout 0 to turn it off.
sr stop --out writes the transcript on the way out. When the session recorded
video, a11ied also writes the Markdown transcript next to the recording file, so you can
read the two together. When cleanup is unconfirmed, retry
sr stop --session-id <id> with the session ID from the error.
The owner blocks further input until cleanup succeeds.
For a single action with no session, add --ephemeral to
press, type, or do. It starts a session, runs the
action, and stops the session in one command. Native actions use a detached broker
so a failed shutdown can be retried after the command exits.
