Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

VoiceOver ARIA Landmark Audit in Safari Guide

Short answer: Audit landmark announcements in Safari because VoiceOver reads the same WebKit accessibility tree Safari renders. Run an osascript loop that advances the VoiceOver cursor through each of the eight WAI-ARIA 1.2 landmark roles, captures content of cursor, writes a TSV, and joins it against your expectations file to emit a WCAG 1.3.6-linked pass/fail CSV. VoiceOver needs a logged-in GUI session, so run this on a hosted Mac you can pin to one OS. The right Figment tier is the M-series plan from $95/month (pricing); exact chip, RAM and disk are confirmed in the quote. The $29 virtual Intel macOS plan runs real macOS and is suitable for Intel Xcode 26-or-earlier toolchains, but its interactive path is the browser-console desktop terminal, not inbound SSH.

Why Safari and Chrome Announce Landmarks Differently

VoiceOver on macOS reads the accessibility tree WebKit builds, which is the same engine that renders Safari. That makes Safari the reference pairing for VoiceOver audits on macOS.

Chrome on macOS uses a different accessibility bridge. Landmark order, role phrasing, and compound names (role + accessible name) can differ between the two. The WAI-ARIA Authoring Practices Group documents VoiceOver + Safari behavior separately from NVDA/JAWS patterns for that reason. If you audit only in Chrome, you can ship an announcement order that confuses VoiceOver users on macOS and iOS. Running the audit in Safari on a real Mac is the only way to capture the spoken strings those users actually hear.

The Eight WAI-ARIA 1.2 Landmark Roles

WAI-ARIA 1.2 defines eight landmark roles. Apple's platform accessibility documentation lives at developer.apple.com/documentation/accessibility.

RoleHTML implicit sourceTypical VoiceOver phrasing (Safari)
banner<header> (top-level)"banner"
navigation<nav>"navigation" + accessible name if present
main<main>"main"
complementary<aside>"complementary"
contentinfo<footer> (top-level)"content information"
form<form> with accessible name"form" + accessible name
region<section> with accessible nameaccessible name + "region"
search<search> or role="search""search"

Exact phrasing varies between macOS releases, so treat this column as the expectation you confirm against your pinned baseline, not as a spec guarantee. VoiceOver's Web Rotor (VO + U) lists landmarks; the audit below walks them directly.

Enabling VoiceOver for Scripted Runs

Open VoiceOver Utility once on the target Mac and tick General → Allow VoiceOver to be controlled with AppleScript. Without that setting, tell application "VoiceOver" calls are refused. You must also grant the controlling process Accessibility permission in System Settings → Privacy & Security → Accessibility.

With those prerequisites in place, you can start and stop VoiceOver from a shell:

# Start VoiceOver (requires AppleScript control to be allowed in VO Utility)
osascript -e 'tell application "VoiceOver" to activate'

# Let VoiceOver finish initializing before driving the cursor
sleep 2

# ... audit script runs here ...

osascript -e 'tell application "VoiceOver" to quit'

VoiceOver cannot run in a headless session; the Mac must be logged in with a visible desktop. On a hosted Mac that means the CI agent must run as a Launch Agent in the logged-in user session, not as a Launch Daemon.

The Audit Script: osascript Iterating Landmarks to TSV

Save the following as scripts/vo-landmark-audit.applescript. Before running it unattended, confirm the "next landmark" key binding in VoiceOver Help on your target macOS version — the default rotor- or commander-assigned shortcut has changed between releases.

-- vo-landmark-audit.applescript
-- Usage: osascript vo-landmark-audit.applescript <url> <out.tsv>

on run argv
    set targetURL to item 1 of argv
    set outFile to item 2 of argv

    tell application "Safari"
        activate
        open location targetURL
        delay 3
    end tell

    tell application "VoiceOver" to activate
    delay 2

    set rows to {"index" & tab & "spoken_text"}
    set seen to {}

    repeat with i from 1 to 20
        tell application "VoiceOver"
            try
                set spokenText to (get content of cursor)
            on error
                set spokenText to "(no cursor content)"
            end try
        end tell

        if seen contains spokenText then exit repeat
        set end of seen to spokenText
        set end of rows to (i as string) & tab & spokenText

        -- Advance cursor to next landmark. Verify this key combo in VO Help
        -- for your installed macOS before relying on it in CI.
        tell application "System Events"
            key code 37 using {control down, option down, command down} -- VO+Cmd+L
        end tell
        delay 0.4
    end repeat

    set outContent to ""
    repeat with r in rows
        set outContent to outContent & r & linefeed
    end repeat

    set fileRef to open for access (POSIX file outFile) with write permission
    set eof fileRef to 0
    write outContent to fileRef
    close access fileRef

    tell application "VoiceOver" to quit
    return "Audit written to " & outFile
end run

Run it:

osascript scripts/vo-landmark-audit.applescript \
  "https://your-staging-url.example.com" \
  "/tmp/vo-audit-$(date +%Y%m%d).tsv"

Example output:

index	spoken_text
1	banner
2	navigation, Main Navigation
3	main, Article content
4	complementary, Related articles
5	content information

This is an unexecuted example, not a benchmark — a failed osascript call returns non-zero and your CI step must stop, not record the failure as a pass.

Turning the TSV Into a WCAG-Linked Pass/Fail CSV

Create audit-expectations.tsv mapping an expected spoken substring to a WCAG criterion:

expected_contains	wcag_criterion	notes
banner	1.3.1	Top-level header must announce as banner
navigation	1.3.1	nav must announce as navigation
main	1.3.1	Single main landmark required
complementary	1.3.1	aside must map to complementary
content information	1.3.1	footer maps to content information
search	1.3.1	search landmark announced

Then join against the audit:

#!/usr/bin/env python3
# scripts/vo-verdict.py <audit.tsv> <expectations.tsv> <output.csv>
import csv, sys

audit_file, expect_file, out_file = sys.argv[1:4]

with open(audit_file) as f:
    audit_rows = list(csv.DictReader(f, delimiter='\t'))

with open(expect_file) as f:
    expectations = list(csv.DictReader(f, delimiter='\t'))

results = []
for exp in expectations:
    keyword = exp['expected_contains'].lower()
    matches = [r for r in audit_rows if keyword in r['spoken_text'].lower()]
    results.append({
        'criterion': exp['wcag_criterion'],
        'expected_contains': exp['expected_contains'],
        'actual_spoken': matches[0]['spoken_text'] if matches else '(not found)',
        'verdict': 'PASS' if matches else 'FAIL',
        'notes': exp['notes'],
    })

with open(out_file, 'w', newline='') as f:
    writer = csv.DictWriter(f, fieldnames=list(results[0].keys()))
    writer.writeheader()
    writer.writerows(results)

failures = [r for r in results if r['verdict'] == 'FAIL']
print(f"Audit complete: {len(results) - len(failures)} PASS, {len(failures)} FAIL")
sys.exit(1 if failures else 0)

The CSV is your evidence file: one row per landmark requirement, with the actual spoken string next to the expected substring.

Running the Audit on a Pinned Hosted Mac

VoiceOver phrasing drifts between macOS versions, so a nightly accessibility baseline needs a machine that stays on one OS until you choose to upgrade. On an M-series Figment host where SSH access is configured in the quote, the orchestrator step is:

ssh ci@mac-host "osascript ~/project/scripts/vo-landmark-audit.applescript \
  'https://staging.example.com' '/tmp/vo-audit.tsv' && \
  python3 ~/project/scripts/vo-verdict.py \
  /tmp/vo-audit.tsv ~/project/audit-expectations.tsv /tmp/vo-results.csv"

scp ci@mac-host:/tmp/vo-results.csv \
  ./audit-evidence/vo-results-$(date +%Y%m%d).csv

If you are evaluating on the $29 virtual Intel macOS plan, run the same two scripts in the VM's browser-console desktop terminal and copy the CSV out via your artifact store — the VM does not expose inbound SSH. VoiceOver itself is available on that macOS install; use the M-series tier when you need Apple-silicon phrasing parity with your users' devices or when you need SSH-driven orchestration.

Picking the Right Plan and Confirming the Price

For a pinned accessibility baseline, the useful Figment tier is the M-series plan from $95/month at /mac-hosting; the exact chip, RAM, disk and remote-access method are quoted per request while supply is confirmed. The Managed Build Mac at $249/month adds CI setup, backups, monitoring, and 30 minutes of monthly support if you want Figment to wire the nightly audit job and keep the Launch Agent healthy.

If you only need a single M4/16GB/256GB Mac and are comparing entry prices, CloudMac publishes $99.99/month and a $999/year prepaid option — that annual price is $83.25 effective per month, below twelve months at the Figment $95 entry. Figment's difference on these audit workloads is the managed CI work in the $249 tier and the ability to pin an OS image for comparable nightly runs; match the plan to whether you want a bare host or the audit pipeline maintained for you.

Start a configuration request at /mac-hosting/start.

Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms