All case studies

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.

Platform Web (SaaS ticketing platform)
Testing tools VoiceOver, keyboard, DOM inspection
Severity Blocked

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-name attribute like data-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 span aria-hidden so 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 disabled attribute and no aria-disabled. They are held back by a single CSS rule, pointer-events: none on 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-disabled rather than the disabled attribute 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 needs role="button", keyboard focus, and handlers for both Enter and Space; role and tabindex alone 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.