Web application accessibility review
Movie theater seat map: it works, but I'm picking seats with missing information.
I go to this theater regularly. The seat map actually works with VoiceOver. I can hear the row letters, seat numbers, and check boxes to pick seats. What I can't hear is whether a seat is already taken, what type it is, or how to jump to a specific row. Sighted users see all of that instantly in a color-coded grid.
What this case study shows
The seat map mostly works, and that is what hid the real problem.
The checkboxes work. The row letters are in the page. The data for seat type and availability is already in the code, it is just never connected to what the screen reader reads. And because my screen reader fills in enough to get by, it took reading the accessibility tree to find that sold seats are not actually blocked for me at all.
What to notice
- The seat map works but is missing context
- Sold seats are blocked by CSS, so only mouse users are actually stopped
- The fix uses data the page already has
- I directed and tested a working before-and-after prototype
- This is a SaaS platform, so one fix helps every theater using it
What already works
Checkboxes are real checkboxes
Each seat is an actual input element. VoiceOver can find them and interact with them.
Row letters are present
The letters A through J are in the page between each row of seats. VoiceOver reads them as you arrow through.
Seat numbers are there
Each checkbox has the seat number as text next to it. VoiceOver picks it up.
Findings
Checkboxes have no accessible name
Usability gap- Issue
- The checkbox inputs have no aria-label, no title, and no associated label element. The seat number sits in a separate span next to the checkbox. At the platform level, these controls have no accessible name at all.
- What I hear
- "14, unchecked checkbox. 13, unchecked checkbox." It sounds fine, whether I read straight through or jump control to control. The number is not coming from the checkbox, though. It is a separate piece of text sitting next to it, and VoiceOver is reading it to describe a control that offers nothing of its own.
- How I verified it
- I stopped trusting what I heard and read the macOS accessibility tree directly. Every naming attribute on these checkboxes comes back empty:
AXTitle,AXDescription,AXHelp,AXTitleUIElement,AXLabelUIElements,AXCustomContent. The name I was hearing is not coming from the page. - Why that matters
- It works, and the page is not the reason it works. The number and the checkbox are related by where they sit, not by anything the page declares, and my screen reader is doing the connecting. That is worth naming rather than glossing over: I tested this expecting the seats to come up blank when I jumped between form controls, and they did not. What proximity cannot rescue is everything with no text beside it. Seat type and whether a seat is sold exist only as CSS class names, so no amount of reading nearby text will ever surface them.
- What's in the code
- Each checkbox carries a
data-nameattribute likedata-name="A14"holding the row and seat. The seat type is in the CSS class on the wrapper. Everything needed for a complete name is already on the element. - Suggested fix
- Build a real name from that data: "Row A, seat 14, love seat, available." Put it in a hidden span, reference it from the input with
aria-labelledby, and mark that spanaria-hiddenso it does not also become browsable text. I built this and measured the result, which is how I found the catch: a visually hidden span inside the label computes the correct name but stays in the accessibility tree, so each seat then announces three times instead of twice. Reviewing the markup does not catch that. Reading the tree does.
Sold seats are blocked for mouse users only
Blocked- Issue
- Sold seats are not disabled. They carry no
disabledattribute and noaria-disabled. They are held back by a single CSS rule,pointer-events: noneon the input. - What that means
- A mouse user physically cannot click a sold seat. I can. The checkbox still takes keyboard focus and still toggles on Space. The rule that says a seat is already taken is enforced visually and nowhere else.
- How I verified it
- Focused a sold seat from the keyboard and confirmed it accepts focus, reports itself as enabled, and carries no disabled state anywhere on the element.
- What I hear
- Nothing that separates it from any other seat. Sold and available sound identical, so there is no warning before I pick one that is already gone.
- Suggested fix
- Put availability in the accessible name so a seat announces as sold before it is selected, and enforce the block for every input method rather than the mouse alone. Use
aria-disabledrather than thedisabledattribute so the seat stays discoverable while browsing, and stop the toggle in script. Test the result across the supported browser and screen reader combinations, because disabled native controls are skipped in some navigation modes.
No way to jump to a specific row
Usability gap- Issue
- If I want Row F, I have to go through every seat in rows A through E first. That's 70+ checkboxes. There's no way to skip ahead.
- Suggested fix
- Make the row labels (A, B, C...) into heading elements. Then VoiceOver heading navigation (VO+Cmd+H) jumps directly to any row.
Seat type not communicated
Usability gap- Issue
- Some seats are love seats, some are wheelchair spaces, some are standard. This is only communicated through CSS classes (LoveSeatLeft, Wheelchair) and visual icons. VoiceOver doesn't know.
- Suggested fix
- Include the seat type in the aria-label. The CSS class already has the information.
Purchase Tickets and Deselect All aren't proper buttons
Usability gap- Issue
- Purchase Tickets is an anchor tag with no href. Deselect All is a div. Neither is a real button element.
- Suggested fix
- Use native
<button>elements. If the platform cannot be changed immediately, the fallback also needsrole="button", keyboard focus, and handlers for both Enter and Space; role andtabindexalone are not enough.
I directed and tested a working prototype
I directed a before-and-after prototype built from the seat map's own HTML and CSS, then tested the result with VoiceOver. Same seats, same layout, with complete seat names, row headings, and sold seats that actually refuse selection.
What the fix actually involves
Complete accessible names
Each checkbox needs a programmatic label built from data already in the page: "Row A, Seat 14, love seat, available."
Row headings
Change the row letter spans to heading elements. No visual change. Adds heading navigation for screen readers.
Disable taken seats
Prevent selection while keeping the unavailable state discoverable, then verify the pattern across supported assistive technology.
Standards context
Relevant WCAG 2.2 criteria
- 1.3.1 Info and Relationships
- 1.4.1 Use of Color
- 2.1.1 Keyboard
- 2.4.6 Headings and Labels
- 4.1.2 Name, Role, Value
Scope note
This was a focused usability review and prototype, not a complete conformance audit of the ticketing platform.
Platform-wide impact
This seat map is part of a ticketing platform used by multiple theaters. The code is shared, so one fix applies everywhere.
Summary
- The data for a full fix is already in the code
- The visual design doesn't need to change
- One fix rolls out to every theater on the platform
- Probably an hour or two of development work
Need this kind of review?
I test websites and apps with VoiceOver and write up what I find.