Search topics
Seat maps keyboards can't use and screen readers can't read
Each seat is an empty div that takes only a click, or a button with no name, and free, taken and chosen seats differ only in colour.
<div class="seat taken"></div> The problem
Keyboard users can't choose a seat, screen reader users can't tell which seat is which or whether it is free, and people who don't see red and green apart can't tell free seats from taken ones
A cinema's booking page shows the seats for one screening as a map: four rows, A to D, six seats in each, under a bar marked Screen. Free seats are green and taken seats red, and a click turns a free seat blue, chosen. A key above the map names the three colours, and a line says that rows A and B cost $10 and rows C and D $14. With a mouse, choosing two seats side by side in row C takes two clicks.
Put the mouse aside and press Tab. Focus goes from the top of the page straight to Continue, past every seat: no seat can be chosen. A screen reader reading the page hears "Screen", then "A", "B", "C" and "D", the row letters, and nothing between them, because the seats are empty divs. And someone who sees red and green as one colour sees a map of seats that look alike: row C's free seats 1, 2, 5 and 6 are the same shade as its taken seats 3 and 4.
The team then turns each seat into a button. Tab now stops on every seat, but a screen reader hears "button" on each of the 24, with nothing to say which row a seat is in, its number or its price. Taken seats are buttons too, and pressing one does nothing, with nothing said. When a seat is chosen, only its colour changes, so the screen reader says "button" again, as it did before.
Success criterion 2.1.1 Keyboard asks that everything a mouse can do, a keyboard can do. Here the only way to choose a seat is to click a div, and a div takes no focus and answers no key, so no seat can be chosen without a pointer.
Success criterion 4.1.2 Name, Role, Value asks that every control has a name and a role, and that its state is exposed to assistive technology. A seat is a control: it can be chosen, and it is free, taken or chosen. The divs have no role and no name, so a screen reader passes over them as if nothing were there. The buttons have a role, but no text and no name. In both, the states are only the classes taken and chosen and their colours. Browsers build what assistive technology reads from elements and attributes, not from class names or background colours, so the state never reaches it.
Success criterion 1.4.1 Use of Color asks that colour is not the only way information is shown. Each seat has only its fill to say whether it is free, taken or chosen: no mark, shape or word. The key above the map has words, but they are matched to the seats by colour alone. The green, the red and the blue are within 1.46:1 of each other in lightness, the red and the blue only 1.11:1, so without their hues they look nearly the same, and to someone with the common red–green colour vision deficiencies, the green and the red look nearly the same.
Cinemas, theatres, concert halls, stadiums, airlines and trains put a map like this in the middle of buying a ticket. When the map is the only way to choose, a seat that can't be chosen is a ticket that can't be bought. Some sites offer a Best available button beside the map, but it chooses for people: they can't ask for an aisle seat, a seat beside the person they came with, or the row they always sit in.
4 of 10 groups affected
- No vision (affected)
- Low vision (affected)
- Colour vision (affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision Screen reader users hear the word Screen and the row letters with nothing between them when the seats are empty divs, and only "button" on each seat when they are buttons. No seat says which row it is in, its number or its price, or whether it is free, taken or chosen, so they can't book a seat without help.
- Limited manipulation Keyboard and switch users can't choose a seat at all when the seats take only a click: Tab goes from the top of the page to Continue, past the whole map.
- Without perception of colour People who don't see red and green apart see free and taken seats as one colour, and find out a seat is taken only when clicking it does nothing. Without any hue, the three colours are almost the same shade.
- Limited vision In a Windows contrast theme, which replaces the fill colours, every seat is drawn the same, and partial sight often comes with weaker colour vision, so free, taken and chosen seats are hard to tell apart.
The empty divs have no role, so axe has nothing to check in the first map, and its colour contrast rule looks at text, not at what a fill colour means. axe does report the empty buttons of the second map, as button-name: each has no text and no name. It can't tell that a button is a seat, or that its classes say it is taken or chosen while its markup says nothing.
The audit looks for seats by their class names, seat and its common variants, in divs, spans and SVG shapes. A seat map where Tab reaches no seat, nor anything else in the map, is reported, and so is one whose seats can be reached but whose class says taken or chosen while the seat has no disabled, aria-disabled, checked, aria-pressed, aria-checked or aria-selected to say so. Every seat map that Tab reaches goes to review, so a person checks that each name gives the row and the number, and that the states show without colour, and so does a canvas whose id or class names it a seat map. A map drawn from shapes with no telling class names is not found, and no check can tell whether a seat's name makes sense to a listener.
Widely copied seat booking tutorials are built the first way, an empty div per seat and one click listener on the map. An AI assistant asked to make one accessible can change the divs to buttons and stop there, which is the second map, or give every seat the same name, such as aria-label="Seat".
Try it
Using only the keyboard, choose two free seats side by side in row C
Broken 1: press Tab. Focus goes past every seat to Continue, and no seat is read. Turn on the simulation: the free and taken seats look alike.
Broken 2: the same seats as buttons. Tab stops on each one, but every seat is just "button", and choosing one changes only its colour.
Apply a fix. With the checkboxes, Tab moves through the free seats and Space chooses one. In the grid, one Tab enters the map and the arrow keys move between seats and rows.
Broken Can you fix this?
Choose your seats
Rows A–B $10 · Rows C–D $14
- Free
- Chosen
- Taken
Screen reader says:
(Tab to the seats)
The fix
Name each seat, reach it by keyboard, and mark its state
Seats as divs that take a click
<h2>Choose your seats</h2><p>Rows A–B $10 · Rows C–D $14</p><ul class="legend"> <li> <span class="key"></span> Free </li> <li> <span class="key chosen"></span> Chosen </li> <li> <span class="key taken"></span> Taken </li></ul><div class="seat-map"> <div class="screen">Screen</div> … <div class="row"> <span class="row-name">C</span> <div class="seat"></div> <div class="seat"></div> <div class="seat taken"></div> <div class="seat taken"></div> <div class="seat"></div> <div class="seat"></div> </div> …</div><button type="button" class="continue">Continue</button>What is wrong4 points
- The seats answer only
clickand take no focus, so Tab goes past the map and no seat can be chosen from the keyboard. - Each seat is an empty div, so a screen reader reads the row letters and nothing between them.
- Free, taken and chosen are classes, so nothing tells assistive technology which is which.
- On screen, the fill colour is the only difference between them.
A checkbox for each seat
<h2>Choose your seats</h2><p>Rows A–B $10 · Rows C–D $14</p><ul class="legend"> <li> <span class="key"></span> Free </li> <li> <span class="key chosen"></span> Chosen </li> <li> <span class="key taken"></span> Taken </li></ul><div class="seat-map"> <div class="screen">Screen</div> … <div class="row"> <span class="row-name">C</span> <label class="seat"> <input type="checkbox" name="seats" value="C1"> <span class="visually-hidden">Row C, seat 1, $14</span> </label> <label class="seat"> <input type="checkbox" name="seats" value="C2"> <span class="visually-hidden">Row C, seat 2, $14</span> </label> <label class="seat"> <input type="checkbox" name="seats" value="C3" disabled> <span class="visually-hidden">Row C, seat 3, $14</span> </label> <label class="seat"> <input type="checkbox" name="seats" value="C4" disabled> <span class="visually-hidden">Row C, seat 4, $14</span> </label> <label class="seat"> <input type="checkbox" name="seats" value="C5"> <span class="visually-hidden">Row C, seat 5, $14</span> </label> <label class="seat"> <input type="checkbox" name="seats" value="C6"> <span class="visually-hidden">Row C, seat 6, $14</span> </label> </div> …</div><button type="button" class="continue">Continue</button>How it is fixed5 steps
- Make each seat a checkbox inside a label, with hidden text that gives its row, number and price. Tab stops on it, Space chooses it, and a screen reader says "Row C, seat 5, $14, checkbox, not checked".
- Disable the checkbox of a taken seat. Tab passes it, and a screen reader reading through the map announces it as unavailable or dimmed.
- Draw the seat on the checkbox itself with
appearance: none, and its colour from:checkedand:disabled, so the look can't drift from what is announced. - Add a tick to a chosen seat and a cross to a taken one, in the seats and in the key, drawn with borders so that they also show in a Windows contrast theme.
- Delete the script. The checkbox keeps its own state, and inside a form, the values of the chosen seats are sent with no code.
This fixes both Befores and passes 1.4.1, 2.1.1 and 4.1.2. Every free seat is a Tab stop, in reading order, row by row.
Why this fix: Use native HTML first
Both pass. For a small room, such as this cinema screen, use the checkboxes: no script and nothing to break, and inside a form the chosen seats are sent even before any JavaScript has loaded. People move through the free seats with Tab, row by row, as through any form. For a large venue, such as a theatre, an aircraft or a section of a stadium, where Tab through hundreds of seats would take minutes and people expect to move to the row behind, use the grid. It costs a script that has to handle every key, and a test with each screen reader, since support for grids varies.
A seat's name is read on its own, as it takes focus, so "Seat 5" or "5" says too little once focus has moved down a row: give the row in every name. Add what changes the choice after the number, such as "aisle", "restricted view", "wheelchair space" or "extra legroom", and the price only where prices differ. The same goes for a seat that holds no seat at all, such as a space for a wheelchair: name it for what it is.
If the seats are already buttons, as in the second Before, the smallest change keeps them: give each the hidden text, aria-pressed, and disabled on taken seats, and draw the states from those attributes. That is the checkbox fix with a button in its place: every free seat a Tab stop, and a screen reader saying "toggle button" rather than "checkbox".
A large venue often opens with a map of its sections, then the seats of the one chosen. Each section needs the same treatment as a seat: a control that the keyboard reaches, named with its name and price range, and its "sold out" in words, not only in grey. A map that can be zoomed and dragged needs buttons for both, as pinch, drag and tilt gestures with no button explains. A seat map drawn on a canvas gives a screen reader nothing at all: draw the seats as elements over it, or offer a list of the free seats, row by row, beside it.
The seats themselves need a 3:1 contrast with what is around them, as low-contrast form fields, icons and focus rings explains; here the green is 3.45:1 on white, and the red and blue more. The marks are white on the red and the blue, at 4.51:1 and 5.02:1. Drawn with borders, the tick and the cross also show in a Windows contrast theme, which removes the fill colours; a mark drawn with a background gradient would disappear there. For other places where colour alone carries a state, such as order statuses, see status dots told apart by colour alone.
Many sites show the seats chosen, and their total, beside Continue: "Row C, seats 5 and 6, $28". That line helps everyone check the choice before paying. A screen reader hears it only when it reaches it, unless it is announced, as add to cart buttons that change a badge and say nothing explains.
Verify the fix
5 checks, no mouse.
Test with: Keyboard, Screen reader, Colour vision simulation, Forced colors
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the seat map, its component or its library changes.
How screen readers handle aria-pressed and role="grid", a button in each cell
Limits
This is a starting pattern, not a guarantee for every application. The grid's script is a minimal sketch: it has no Page Up and Page Down, and no way to jump to a row by its letter, which a large venue may need. NVDA has been reported to leave focus mode when the arrow keys move to a link in a grid cell, after which the arrows move its reading cursor instead; if it does the same with these buttons, NVDA users need Insert + Space to get the arrow keys back, so test the grid with NVDA in particular. A venue drawn on a street map, such as a list of cinemas, is covered in maps that are the only way to find a location. A grid whose cells are edited, rather than chosen, is covered in editable data grids a keyboard can't move through or edit, and checkboxes drawn by hand in a form in custom checkboxes that keyboards and screen readers skip. Test the complete journey, from choosing a showing to paying, with your target assistive technology.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: use-of-color (opens in a new tab)
- WCAG Understanding: keyboard (opens in a new tab)
- WCAG Understanding: name-role-value (opens in a new tab)
- ARIA APG: grid (opens in a new tab)
- ARIA APG: layout-grids (opens in a new tab)
- ARIA APG: button (opens in a new tab)
- developer.mozilla.org: checkbox (opens in a new tab)
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.