
Posted on
It started with a simple idea. Can I explain to stakeholders just how bad the experience of using their software is for someone using a screen reader?
I've been auditing a number of applications for compliance with WCAG 2.1 AA to help organizations understand their risks with the upcoming Department of Justice ADA Ruling looming. In some of the conversations with stakeholders, they don't always understand what the lack of support for these standards truly means for someone using an assistive device. The results were truly astounding and enlightening.
I've anonymized the prompt and the result by removing the agency name, but I left in a reference to the application name. This is not to say that all implementations of Accela are not accessible, but this particular instance clearly needs some remediation.
The prompt I gave Claude Code Sonnet 4.6 was very simple:
Using the chrome browser I just opened, can you visit the [redacted] application and provide a textual description of what a visitor using NVDA screen reader software would experience using the application. Write it as a transcript from the point of view of that user.
Here is the full transcript it gave me.
Permit Search Application — NVDA Screen Reader Experience Transcript
Environment: NVDA 2024, Chrome, Windows | Browse mode (default)
Coverage: Homepage → Search Applications (Building Permits)
Part 1: Homepage
The user opens Chrome and navigates to the application homepage.
"Accela Citizen Access"
The page title gives no indication this is a government service or what the application is for.
NVDA begins reading from the top of the page in browse mode.
"[agency-logo] dot P N G graphic"
The agency seal has no alt attribute. NVDA reads the image filename letter by letter. The user has no idea this is an agency logo.
The user presses Tab to reach the first interactive element.
"Skip to main content. Please make sure the Accessibility Support check box is marked. link"
The skip link text is disorienting. It directs the user to mark an "Accessibility Support check box" that does not exist anywhere on the page. A screen reader user expecting a standard skip link instead receives confusing instructions. The link target is # — activating it moves focus nowhere.
Tab again:
"Skip Module Navigation link"
Tab again. NVDA announces entering the navigation landmark:
"navigation landmark"
"home Home link"
The material icon text "home" is rendered as literal text in the DOM, so NVDA reads it as part of the link name. The link itself is functional.
The user presses Tab to continue. The "Search", "New", and "Schedule" menu labels are generic <div> elements — not buttons or links — so they receive no keyboard focus and are silently skipped. The user lands directly inside the open dropdown items.
"Building Permits link"
"Current Planning Applications link"
"Long Range Planning Applications link"
"Compliance Records link"
"Septic Permits link"
"Right-of-Way Records link"
"Public Improvement Records link"
"Traffic Engineering Permits link"
These eight links are functional and go to real URLs. However, because the "Search" dropdown trigger was unreachable, the user has no idea these links are grouped under a Search menu. There is no indication of their context.
Tabbing continues into the utility bar:
"Report List link"
This link uses href="javascript:void(0);". NVDA announces it as a link, but it has no real destination.
"Fire Marshals Final Combldg link"
"Inspection History Result link"
"Issued Event Permits link"
"Issued Permits with Backflow link"
"Project Related Activities link"
"Scheduled Inspections Contact Information link"
"TACS Permits ByIssued Data Import link"
"TACS Permits Data Import link"
Eight more javascript:void(0) links. In a screen reader's link list, these appear alongside real navigation links with no way to distinguish them from genuine URLs.
"Register for an Account link"
"Announcements link"
"left parenthesis right parenthesis link"
An anonymous link rendered as () — the remnant of an empty notification badge. Its purpose is completely opaque.
"Announcement List link"
"Mark as Read link"
The user reaches the search bar:
"Search... edit"
"Search button"
"Search History link"
The user reaches the module tab strip:
"Building Permits link"
"Current Planning link"
"Long Range Planning link"
"Facility Permits link"
"Septic Permits link"
"Right-of-Way link"
"Traffic Eng link"
"Code Compliance link"
All eight use href="javascript:void(0);". None carry an aria-selected state or aria-current attribute. The user cannot tell which tab is currently active.
Tab continues to the sub-navigation:
"search Search Applications link"
"schedule Select Record and Request an Inspection link"
The material icon names "search" and "schedule" are read as part of each link's text. The user hears "search Search Applications" and "schedule Select Record and Request an Inspection."
The user reaches the main content. NVDA enters a layout table:
"table with 1 row and 1 column"
"[Agency staffing notice text]"
"[Notice continues — services may take longer than usual]"
"How to Register for a New Account link"
"How to Search for Information link"
"How to Request an Inspection link"
"What would you like to do today?"
"To get started, select one of the services listed below:"
The user continues through the service cards. Each category is wrapped in its own layout table:
"table with 1 row and 1 column"
"Building Permits"
Not a heading — just text. The user cannot navigate to it with the H key.
"search Search Applications link"
"schedule Select Record and Request an Inspection link"
"table with 1 row and 1 column"
"Current Planning"
"search Search Applications link"
"table with 1 row and 1 column"
"Long Range Planning"
"search Search Records link"
"table with 1 row and 1 column"
"Facility Permits"
"search Search Records link"
"schedule Select Record and Request an Inspection link"
"table with 1 row and 1 column"
"Septic Permits"
"search Search Records link"
"schedule Select Record and Request an Inspection link"
"table with 1 row and 1 column"
"Right-of-Way"
"search Search Records link"
"schedule Select Record and Request an Inspection link"
"table with 1 row and 1 column"
"Traffic Eng"
"search Search Applications link"
"table with 1 row and 1 column"
"Code Compliance"
"note_add Create a Record link"
"search Search Records link"
By this point the user has heard "search Search Applications link" or "search Search Records link" ten times, all announced identically. Because the category labels are not headings, navigation by heading is impossible. The user must Tab through the entire list to find the right category.
The user reaches the Login Frame iframe:
"Login Frame frame"
The user presses Enter to enter the frame:
"Username or email: colon star edit"
The label is properly associated. The asterisk for a required field is read as "star."
"Password: colon star password"
"Forgot Password? link"
"SIGN IN button"
"Remember me on this computer check box not checked"
The user presses Escape to exit the frame and continues to the footer:
"table with 3 rows and 2 columns"
"row 1, column 1. [Agency Address]"
"row 1, column 2. This page maintained by: [Department Name] link"
"row 2, column 1. Phone: [Agency Phone]"
"row 2, column 2. Disclaimer and Privacy Statement link"
"row 3, column 1. Email: [Department Name] link"
NVDA treats the footer as a data table and announces row and column positions for plain contact information.
Part 2: Search Applications
The user activates the "search Search Applications link" under Building Permits and waits for the page to load.
"Accela Citizen Access"
The page title is identical to the homepage. The user receives no confirmation they have navigated to a new page or what that page contains.
The user presses H to navigate by headings and orient themselves:
"Message Bar heading level 1"
This is a visually hidden notification container — not a useful orientation point.
"Search for Records heading level 1"
"General Search heading level 1"
The page has three H1 elements and no subordinate headings. The heading structure conveys no hierarchy and cannot help the user understand how the form is organized.
After tabbing through the repeated navigation — identical to the homepage — the user reaches the main search form with no announcement that they have arrived at it:
"Search All Records check box not checked"
The user's first interaction with the form is a checkbox with no surrounding context. No heading or group label announces that a search form has begun.
"Record Number: edit"
"Record Type: — Select — combo box"
After a short delay, NVDA reads the title attribute:
"Select a dropdown value to refresh dropdown options in associated fields or refresh the Additional Criteria Form. Keyboard Operating Instruction: Please hold ALT and press UP ARROW to expand the list, then press UP ARROW or DOWN ARROW to select an item, and finally press ENTER."
This 45-word tooltip is announced every time the user focuses this field. The embedded keyboard instruction is also inaccurate for standard NVDA and Chrome — the correct method is Alt+Down Arrow or Space, not the sequence described.
The dropdown has an onchange event handler. If the user presses a Down Arrow key to explore options and inadvertently lands on a selection, the page reloads and the user's other entries may be lost.
"Project Name: edit"
"Start Date: M M slash D D slash Y Y Y Y edit"
"End Date: M M slash D D slash Y Y Y Y edit"
"License Type: — Select — combo box"
"Keyboard Operating Instruction: Please hold ALT and press UP ARROW to expand the list, then press UP ARROW or DOWN ARROW to select an item, and finally press ENTER."
"State License Number: edit"
"Name of Business: edit"
"Business License # edit"
"Street No. From edit"
This field's accessible name comes from its title attribute only — there is no <label> element. The visible "Street No.:" label on screen is in a separate table cell and is not programmatically associated.
"Street No. To edit"
Same situation. The user hears two fields identified only as "From" and "To" with no stated relationship to each other.
"Direction: — Select — combo box"
"Keyboard Operating Instruction: Please hold ALT and press UP ARROW…"
Tab through Street Name, Street Type, Unit No., City, State, Zip, and Parcel No. — all labeled correctly.
"Search button"
"Clear button"
Summary
A screen reader user attempting to search for a building permit would need to tab past approximately 40 interactive elements — skip links that go nowhere, navigation dropdowns whose triggers are unreachable, eight module tabs with no selected state, and ten repeated "search Search Applications" links — before reaching the search form. Once there, the page title gives no confirmation they are in the right place, the heading structure offers no way to orient within the form, and the Record Type dropdown's onchange behavior risks resetting their work if they attempt to explore its options with the keyboard.
What this means for accessibility testing
This completely changes what accessibility testing looks like for applications. AI-generated transcripts are just the start. I could easily shift the prompt to provide feedback on what I need to change to achieve compliance with WCAG 2.1. Want to take it a step further? I could have it automatically create the issues in my developer backlog to address and have it take a stab at making those fixes.
Suddenly, there is no excuse for building an application that is inacessible to screen readers. And I could apply this same approach for any other accessibility accommodations.