EverProduct
Frontend

Stage 02 · A Page That Holds Up

The Second Interface: Accessibility in Code

Your page ships with two interfaces — the one on screen and the one machines read aloud. The second is generated by your code whether you thought about it or not.

Turn the monitor off and open your own site with a screen reader. Most developers have never done this, and the first thirty seconds are educational in a way no article can be.

What you hear is something like: "button. button. clickable. link. image, 4382 dot jpg. clickable. edit text. button."

That is your other interface. Not a special mode you can enable later — it exists already, it was generated from your markup, and right now it's the product some people are using. The UX sphere makes the case for why this matters and who it affects. This article is about the mechanics: what in your code produces that second interface, and how to stop it being noise.

The accessibility tree

Alongside the DOM, the browser builds an accessibility tree — the same page reduced to what assistive technology needs: for each element, a role (what is it), a name (what is it called), a value, and a state (pressed, expanded, disabled, invalid).

You can look at it. Every browser's devtools has a panel for it, and reading your own page there is the fastest way to see what you've actually built. Elements missing a role, controls with no name, whole regions absent — it's all visible in a few seconds.

Everything below is just ways of filling that tree correctly.

The first rule is: use the right element

Most accessibility is decided before ARIA enters the conversation. A button element already has the role "button", takes focus, responds to Enter and Space, and announces its label. A nav, main and h2 already produce landmarks and an outline.

Which leads to the rule that saves the most trouble: the best ARIA is no ARIA. Reach for a native element first, every time, and add ARIA only where the platform has no equivalent.

The reason is a fact people learn far too late: ARIA changes what is announced, not what happens. Putting role="button" on a div makes a screen reader say "button" — and the div still doesn't take focus, still doesn't fire on Enter, still isn't in the tab order. You've made a promise the element can't keep, which is worse than saying nothing at all.

ARIA is a label, not a wire. It renames the thing; it doesn't connect it.

The short version of the official rules is worth memorising: don't use ARIA when a native element exists; don't override native semantics (a role="heading" on a button helps nobody); anything interactive must work from the keyboard; never put aria-hidden on something focusable; and every interactive element needs an accessible name.

Names: the most common failure

An element's accessible name is what gets announced. It comes from, roughly in order: an associated label, aria-labelledby, aria-label, or the element's own text content.

The failures are predictable and everywhere:

  • Icon-only buttons. A close button containing an SVG and nothing else is announced as "button". aria-label="Close" fixes it in ten characters.
  • "Read more" twelve times. Screen reader users often navigate by pulling up a list of every link on a page. Twelve identical "read more" entries is a list of twelve unusable options. Make the link text carry the destination.
  • Images. A missing alt gets the file name read out. A decorative image needs an empty alt, which means "skip this."
  • Form fields with a placeholder and no label. Covered in the forms article, and still the single most common defect on the web.

Focus is the whole keyboard story

For anyone not using a mouse — keyboard users, screen reader users, people with tremor, and anyone whose trackpad just died — focus is the interface.

Never remove the focus outline without replacing it. outline: none is in more stylesheets than any other accessibility bug on earth. If the default ring is ugly, design a better one with :focus-visible, which shows it for keyboard use and not on mouse clicks.

Focus order follows the DOM, so a visual order achieved by rearranging things in CSS creates a tab sequence that jumps around the screen. Positive tabindex values make it worse; the only values you need are 0 (focusable in order) and -1 (focusable only by script).

Manage focus when the page changes under it. Open a modal — move focus into it, trap it there, return it to the trigger on close. Delete the row that had focus — move focus somewhere sensible, or the user is dropped back to the top of the document. Navigate in a single-page app — nothing reloads, so nothing announces the new page unless you tell it to.

Add a skip link. One link at the top, visible on focus, that jumps to the main content, so a keyboard user doesn't traverse forty menu items on every page.

Announcing what changed

A visual user notices a toast appearing in the corner. A screen reader user notices nothing at all — a change in the DOM is silent unless you mark the region that changed.

That's what live regions are for: aria-live="polite" announces after the current speech finishes and is right for almost everything — search result counts, "saved", validation summaries. aria-live="assertive" interrupts and should be reserved for things that genuinely can't wait.

Three practical notes: the container must exist in the DOM before the content changes, keep the message short, and don't wire one up to something that updates constantly unless you enjoy building a machine that talks over itself.

Components where people get hurt

Some widgets have no native equivalent and are genuinely hard: combobox with autocomplete, tabs, tree views, custom selects, date pickers, drag and drop, carousels. Each has a documented keyboard contract — which arrow key does what, what Escape does, what gets announced — and getting it right takes real work.

The honest advice: use native elements where they exist (dialog, details, a real select), and for the rest, use a well-tested accessible component library rather than writing your own. A hand-rolled combobox is one of the most reliable ways to make a page unusable while believing you've done something clever.

The rest of the checklist

Contrast. Text needs a 4.5:1 ratio against its background (3:1 for large text), and so do the visual boundaries of controls. Devtools tells you the number; it's not a matter of opinion.

Never colour alone. A red border on an invalid field is invisible to a lot of people. Add an icon, text, or both — the message is the fix.

Touch targets of about 44 pixels, with space between them.

prefers-reduced-motion — for some people animation causes actual nausea and dizziness. One media query, and you swap the movement for a fade.

Language. lang on the html element, and on any element in a different language, or a screen reader pronounces it with the wrong phonetics.

Zoom to 200% without losing content or function. This is far more commonly needed than screen readers, and far more commonly broken.

Testing: cheap, then honest

Automated tools — axe, Lighthouse, the browser's own audit — take a minute and catch a genuine set of mistakes: missing alt text, low contrast, missing names, invalid ARIA. Run them. Then remember the number that matters: they catch roughly a third of real problems. Whether the tab order makes sense, whether the announcements are comprehensible, whether the modal returns focus — no tool can judge any of that.

Which makes a green audit a textbook illusion of competence in the How to Learn sense: a pleasant signal that measures the wrong thing and produces confidence you haven't earned. The three checks that actually work are manual, and none takes long:

  1. Unplug the mouse. Do the main task keyboard-only.
  2. Tab through and watch the focus ring. Can you always see where you are? Does the order match the visual layout?
  3. Turn on the screen reader for five minutes. VoiceOver is built into macOS and iOS, Narrator into Windows, NVDA is free. You'll be terrible at it — that's not the point. The point is hearing your own page.

In practice

Attach a keyboard pass to something you already do. The How to Learn sphere's rule for habits is that the cue matters more than the intention: "finish a component, tab through it" is a cue you'll actually meet. Deciding to "care about accessibility" is not.

Read the accessibility tree of a component you just built. Ten seconds, and it shows you exactly what you shipped.

Fix the three that cover most of it: every interactive element has a name, focus is visible everywhere, nothing needs a mouse.

Learn one screen reader shortcut a week. Twenty minutes total, spaced out — that's deliberate practice on the thing you can't do yet rather than more reading about the thing you can.

Add accessibility to your definition of done, in the same list as "works on mobile". Anything not in that list gets done never.

Check yourself

Close the article and answer in your own words:

  1. What is the accessibility tree and what does it store for each element?
  2. Why is role="button" on a div worse than leaving it plain?
  3. Where does an accessible name come from, and name three common ways it goes missing.
  4. What's wrong with outline: none, and what should you use instead?
  5. When does focus need to be moved by your code?
  6. What is a live region for, and when is assertive justified?
  7. Why is a passing automated audit an illusion of competence, and what three manual checks replace it?

In short

  • Every page has a second interface, generated from your markup into the accessibility tree — role, name, value, state — and you can read it in devtools.
  • Native elements provide most of it for free; the best ARIA is no ARIA.
  • ARIA changes what is announced, not what happens: a role without behaviour is a promise the element can't keep.
  • Accessible names are the most common failure — icon buttons, repeated "read more" links, missing alt, unlabelled fields.
  • Focus is the interface for non-mouse users: keep it visible, keep the order sane, manage it on modals, deletions and route changes, and add a skip link.
  • Announce asynchronous changes with a live region that exists before the change.
  • For complex widgets use native elements or a well-tested library; hand-rolled comboboxes are where pages become unusable.
  • Automated tools catch about a third; the real checks are keyboard-only use, watching the focus ring, and five minutes with a screen reader.