Accessible, fast pages
Ama checks her stock screen at 7 am on a cheap Android phone over patchy mobile data. One of her supervisors uses a screen reader, and another works only with a keyboard. This lesson shows you how to make one screen work for all of them and feel instant on slow hardware, by measuring first and then fixing the part that is really slow.
By the end you can
- Build screens from semantic HTML (buttons, tables, headings, labels, landmarks) and use ARIA only where HTML has no answer
- Move focus when a panel opens and closes, and announce "Saved" with a live region
- Explain LCP, INP and CLS, their "good" thresholds, and why they are judged at the 75th percentile of real visits
- Find a slow interaction with the React DevTools Profiler and the Performance panel under CPU throttling
- Fix a slow 2,000-row list with memo,
useDeferredValueand virtualization
The everyday picture
Think about what makes a store work for every customer. Shelf labels say what is on each shelf, so anyone can find the rice, even a customer who asks a staff member to read the labels aloud. There is a ramp next to the steps. When a new till opens, someone calls out "Till 3 is open" once, calmly, and nobody repeats it every 30 seconds.
Speed in a store has three parts too: how long until you can see the shelf you came for, how long between asking a question and getting an answer, and whether the shelves stay where they are while you reach for a bag of flour.
A web page has the same parts. Semantic HTML is the shelf labels, telling a screen reader what each thing is. Keyboard support is the ramp. A live region is the calm "Till 3 is open". And the three speeds are the Core Web Vitals: LCP (you see the shelf), INP (you get an answer) and CLS (the shelves stay put).
Semantic HTML first, ARIA second
A screen reader does not see your pixels. The browser builds an accessibility tree from your HTML, and the screen reader reads that tree. Every node has a role (button, table, heading), an accessible name and a state (pressed, expanded, invalid). If your HTML says the wrong thing, so does the tree, and CSS cannot fix it.
The classic bug is a clickable <div>. A mouse click works, but in the tree it is a generic container with no role, Tab skips it because a div cannot take focus, and Enter and Space do nothing. A real <button type="button"> gives you all three for free: it takes focus, fires onClick on Enter and Space, and is announced as a button. The same thinking covers the rest of the stock screen:
- Tabular data goes in a
<table>with<th scope="col">headers and a<caption>. A screen reader user can then move cell by cell and hear "On hand, 3" instead of a stream of numbers. - Headings and landmarks: one
<h1>, then<h2>for "Low stock" and "Today's app orders", inside<header>,<nav>and<main>. Many screen reader users jump between these first to learn the shape of a page. - A visible
<label>for every input, linked withhtmlForandid. Placeholder text disappears as soon as you type, so it cannot replace a label. - A useful name for every repeated button. An "Adjust" button in 2,000 rows has to say which item it adjusts: add visually hidden text so it reads "Adjust Rice 5 kg", or use
aria-label.
That last case is where ARIA comes in. ARIA attributes change what the tree says, and only that. role="button" on a div makes a screen reader say "button", yet Tab still skips it and Enter still does nothing. This is why the W3C's guide to using ARIA starts with the first rule of ARIA: if a native HTML element already has the meaning and behaviour you need, use it instead of adding ARIA to something else. Reach for ARIA where HTML has no element: aria-live for announcements, aria-expanded on a button that shows a panel, aria-describedby for field errors (Forms that people can trust).
Colour belongs here too. WCAG 2.2 at level AA, the level most laws and contracts point to, asks for a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. And never let colour carry meaning alone: the low-stock badge says "Low" in words as well as being amber.
Quick check
A screen reader user tabs through the stock list and hears "Adjust, button" again and again. What is wrong?
They are real buttons, which is good, but every one has the same name. Add visually hidden text or an aria-label so each reads "Adjust Rice 5 kg". Adding role="button" to a <button> changes nothing, because it already has that role.
<button> with hidden item text gives them a named button that every key reaches.