Semantic HTML first
Using the HTML element that already means what you need, like button, a, nav, and h2, so keyboards, screen readers, and voice control understand the page with no extra code.
The problem: a page that only works for eyes and a mouse
Picture a checkout page built by a busy team. The "Place order" control is a div with a click handler and a blue background. It looks like a button, and with a mouse it works. Most of the team never tries anything else, so the bug ships.
Now try the same page three other ways. A keyboard user presses Tab and focus jumps straight past the control, because a div can't take focus. A screen reader user hears "Place order" read as plain text, with no hint that it does anything. A voice control user says "click Place order" and nothing happens, because the software looks for buttons and links and finds none with that name.
Nobody wrote a bug on purpose. The team picked an element that carries no meaning, then rebuilt only the visible half of a button. The other half, the part that tools and people without a mouse rely on, was never there.
What "semantic" means here
Semantics is what an element means, separate from how it looks. A button means "pressing this does something", and an a with an href means "this takes you somewhere". A nav means "this is a group of links for getting around", and an h2 means "a new part of the page starts here".
The browser turns that meaning into two things. First, behavior: a button gets keyboard focus, fires click on Enter and Space, and stops working when disabled. Second, information: the browser publishes the element's role, name, and state to assistive technology, the software and hardware people use in place of (or alongside) a screen, mouse, or keyboard.
Semantic HTML first is the habit of reaching for the element that already means what you need before writing any custom code. It is the cheapest accessibility work there is: zero extra JavaScript, zero extra attributes, and the browser keeps it correct for you as standards change.
How common the failure is
This is not a rare edge case. Every year WebAIM, a nonprofit at Utah State University, runs automated checks on the home pages of the top one million websites and publishes the results as the WebAIM Million. In the 2026 report, 95.9% of home pages had failures a tool could detect, with an average of 56.1 errors per page. That is before a human checks anything a tool can't.
The six most common failures have been the same for seven years: low contrast text (83.9% of pages in 2026), images with no text alternative (53.1%), form inputs with no label (51%), empty links (46.3%), empty buttons (30.6%), and a missing page language (13.5%). Four of those six are about using the right element and giving it a name. Native HTML used well prevents them before they exist.
Who is affected? Screen reader users who are blind or have low vision, keyboard users (including people with tremors or limited hand movement, and many power users), voice control users, switch users who drive the page with one or two buttons, and people who zoom to 400%. And everyone at some point: a broken trackpad, a bright sunny screen, a baby in one arm.
Check your understanding · Exercise 1
A "Delete" control is a div with an onClick handler and cursor: pointer. Which users can NOT use it as built? Select all that apply.
The mental model: the accessibility tree
Keep one picture in your head: the browser builds two trees from your HTML. The DOM tree (Document Object Model) is what your JavaScript reads and changes. The accessibility tree is a smaller, simpler copy made for assistive technology. Screen readers, voice control, and switch software never look at your pixels or your CSS classes; they read the accessibility tree.
An everyday comparison: the DOM is the full blueprint of a building, with every pipe and wire. The accessibility tree is the floor plan on the wall by the lift: just the rooms, their names, and the doors. A visitor who can't see the building relies on that floor plan, so if a door is missing from it, that door does not exist for them.
The four facts each node carries
Every node in the accessibility tree is described by a few facts. Learn these four words; the rest of the module uses them.
- Role: what kind of thing it is, like
button,link,heading,textbox,checkbox,navigation. The role tells the user what they can do with it. - Name: what it is called, like "Place order" or "Search". It comes from the text inside, a
label, analt, or ARIA. A control with no name is announced as just "button", which is useless. - State: things that change, like pressed, expanded, checked, selected, disabled, invalid.
- Value: the current content of a control, like the text in an input or the position of a slider.
A screen reader turns these facts into speech. A button with the text "Save" is read as "Save, button". An input type="checkbox" with a label "Remember me" that is ticked is read as "Remember me, checkbox, checked". The exact words differ between screen readers, but the facts underneath are the same.
How the browser builds it
The browser walks the DOM, and for each element it asks: does this element mean something, and can the user see it? The rules are written down in the HTML-AAM spec (HTML Accessibility API Mappings), which lists, for each HTML element, which role it maps to.
- Hidden things are dropped. Elements with
display: none,visibility: hidden, thehiddenattribute, oraria-hidden="true"do not appear, and neither do their children. - Each element gets its implicit role.
buttonbecomesbutton,a hrefbecomeslink,h2becomesheadingwith level 2,ulbecomeslist,navbecomesnavigation. - Meaningless wrappers are flattened. A
divorspanhas the rolegeneric. Browsers often skip generic nodes and pass their children up, so ten nesteddivs leave almost no trace. - Names are computed. For each node that needs one, the browser runs the accessible name algorithm:
aria-labelledbyfirst, thenaria-label, then the native source (alabel,alt,caption,legend), then the text inside, thentitle. For a text input,placeholdercomes last of all. - ARIA overrides are applied. A
roleoraria-*attribute replaces or adds to what the HTML gave. This is powerful and dangerous, as chapter 7 shows. - The result is handed to the operating system. Each platform has its own accessibility API: UI Automation on Windows, the NSAccessibility API on macOS, AT-SPI on Linux. The screen reader talks to that API, not to the browser directly.
See it yourself
You don't need a screen reader to look at the tree. In Chrome DevTools, open the Elements panel, pick an element, and open the Accessibility pane: it shows the role, the computed name, and where the name came from. Chrome also has a full-page accessibility tree view you can switch on from the same pane. Firefox has an Accessibility panel with the whole tree and a checker for common issues.
Think first
A card is built as div with a click handler, holding an img with no alt, a div with the title text, and a span with the price. What does it look like in the accessibility tree?
Close to nothing useful. The outer div is generic, so it is flattened and its click handler is invisible. The image with no alt may be announced by its file name, or skipped. The title and price come through as two loose pieces of text.
A screen reader user hears some text but gets no hint that the card can be opened, and a keyboard user can't reach it at all. Wrapping the title in a heading and a real link, like h3 containing a href, fixes both in one move.
What native elements give you for free
The strongest argument for semantic HTML is a list. Here is everything a plain button does that you get without writing a single line of code. Then here is what it costs to rebuild the same thing on a div.
The button, item by item
- Focus. It is in the tab order.
Tabreaches it,Shift+Tabgoes back, and the browser draws a focus ring when the user is on the keyboard (the:focus-visiblestate). - Keys.
Enterfiresclickas soon as the key goes down.Spacefiresclickwhen the key comes back up, and moving focus away before release cancels it, the same way dragging the mouse off a button cancels a click. - Role and name. It is announced as "button", with its text as the name. Voice control can target it: "click Save".
- Disabled. With
disabled, it leaves the tab order, stops firingclick, and is announced as unavailable or dimmed. - Forms. Inside a
form, a button submits it, and pressingEnterin a text field of that form submits it too (called implicit submission). It works before JavaScript loads. - System colors. In Windows contrast themes (forced colors mode), the browser repaints buttons with system colors so they stay visible.
Try it with the explorer below. Pick an element, read what it gets for free, then use your real keyboard on the sample. All five samples look alike; watch which keys reach them and which ones they answer.
| Role in the accessibility tree | button |
|---|---|
| Reached with Tab | Yes |
| Enter activates it | Yes |
| Space activates it | Yes, when the key is released |
| Can be disabled | Yes, with disabled |
| A screen reader says | “Save, button” |
Try the sample: press Tab to reach it, then press Enter and Space. Then click it. All five samples look the same; they don't behave the same.
The diagram below follows one key press on a native button from start to finish. Notice how many messages the browser sends for you, and to whom.
button. With a div, the browser sends none of them: you would write the Tab stop, the key handling, and the role yourself.The cost of rebuilding it
To make a div act like a button you need at least the code below, and it still misses things: it doesn't submit forms, it doesn't get system colors in forced colors mode, and every team that copies it introduces its own small bugs.
// What a div needs to pretend to be a button
<div
role="button" // announce it as a button
tabIndex={disabled ? -1 : 0} // put it in the tab order
aria-disabled={disabled} // announce disabled
onClick={disabled ? undefined : onPress}
onKeyDown={(e) => {
if (e.key === " ") e.preventDefault(); // stop page scroll
if (e.key === "Enter" && !disabled) onPress();
}}
onKeyUp={(e) => {
if (e.key === " " && !disabled) onPress();
}}
>
Save
</div>
// The same thing, done right
<button type="button" disabled={disabled} onClick={onPress}>Save</button>
It is not only buttons
Many native elements carry a whole interaction pattern with them. Before building a custom widget, check this table.
| Element | What you get for free | Common way to lose it |
|---|---|---|
a href | Link role, focus, Enter to follow, open in new tab, copy link, history, prefetch by the browser | Leaving out href, or using onClick with no href |
input, textarea | Text editing, selection, undo, autofill, spell check, the right mobile keyboard for type="email" or inputmode | A div with contenteditable for a simple field |
input type="checkbox", radio | Checked state, Space to toggle, arrow keys move between radios in a group | Styled spans with an icon |
select | A listbox that works with keyboard, screen readers, and the native picker on phones | A custom dropdown built from divs |
details and summary | Expand and collapse, the expanded state announced, Enter and Space to toggle | A div that toggles a class |
dialog with showModal() | Focus moved in, the rest of the page made inert (unreachable), Escape to close, drawn above everything in the top layer | A positioned div with a backdrop |
popover attribute | Drawn in the top layer, closes on an outside click or Escape (called light dismiss) | Manual z-index and click-outside listeners |
Native elements also improve over time without your involvement. When browsers added the popover attribute and better dialog behavior, sites using those elements got the improvements for free. Custom div widgets stay exactly as good, or as bad, as the day they were written.
aria-disabled check you write and maintain. The bottom row is the most work for a result that is still worse than the top row.Lab: div soup against semantic markup
The lab below shows the same small page built two ways. Switch between them and read the accessibility tree on the right. Then press the simulated Tab key and watch where focus lands, and try Enter and Space on each control.
The HTML
<div class="header">
<div class="nav">
<span onclick="go('/')">Home</span>
<span onclick="go('/pricing')">Pricing</span>
</div>
</div>
<div class="main">
<div class="title">Orders</div>
<input placeholder="Search orders">
<div class="btn" onclick="save()">Save</div>
</div>
<div class="footer">Shop Ltd</div>The accessibility tree (the highlighted line is where focus or the reading cursor is)
document
text “Home”
text “Pricing”
text “Orders”
textbox “Search orders” (from the placeholder only)
text “Save”
text “Shop Ltd”
Tab stops: 1. Landmarks: 0. Headings: 0.
Press Tab to start.
Check your understanding · Exercise 2
Which of these does a native button type="button" give you without extra code? Select all that apply.
Landmarks and headings: the map of the page
A sighted user glances at a page and sees its shape in under a second: the header, the menu, the main column, the sidebar. A screen reader user gets that same shape from two things: landmarks (the big regions) and headings (the outline inside them). Get both right and a screen reader user can jump straight to what they want, the way your eyes do.
Landmarks and their implicit roles
Eight HTML elements can create landmarks. Each maps to an ARIA landmark role, so you never need to write that role yourself.
| Element | Implicit role | When it counts as a landmark |
|---|---|---|
header | banner | Only when it is not inside article, aside, main, nav, or section. Inside those it is not a landmark. |
nav | navigation | Always. Label it when there is more than one. |
main | main | Always. Exactly one visible main per page. |
aside | complementary | At the top level or inside main. Inside article, aside, nav, or section, only when it has an accessible name. Use it for content related to the main content, like a sidebar. |
footer | contentinfo | Same rule as header: not a landmark inside article, aside, main, nav, or section. |
section | region | Only when it has an accessible name, for example from aria-labelledby pointing at its heading. |
form | form | Only when it has an accessible name. |
search | search | Always. A newer element that wraps a search form; supported in all major browsers. |
role="banner". The two navs carry labels so a screen reader user can tell them apart in the landmark list.Screen readers let users jump by landmark: in NVDA the D key moves to the next landmark, and VoiceOver lists landmarks in its rotor. If two landmarks have the same role, label them so they are told apart, like nav aria-label="Primary" and nav aria-label="Breadcrumb". Leave the word "navigation" out of the label, because the role already says it and the user would hear "Primary navigation navigation".
Landmarks only help screen reader users. A sighted keyboard user still has to press Tab through every header link to reach the content, so add a skip link: the first focusable element on the page, an a href="#main" that says "Skip to main content" and becomes visible when focused. In the 2026 WebAIM Million, only 46.1% of home pages had a main landmark, 17.1% had a skip link, and one in ten of those skip links was broken.
Headings: the outline
Headings are the most used way to move around a page with a screen reader. In WebAIM's screen reader user surveys, navigating through headings is by far the most common first step when looking for information on a long page: 71.6% of respondents chose it in the 2024 survey. In NVDA and JAWS the H key jumps to the next heading, and 1 to 6 jump to the next heading of that level.
- One
h1that names the page. Usually the same idea as thetitle, like "Invoice 1042". Not the site logo on every page. - Don't skip levels going down. After an
h2, the next deeper heading ish3, noth4. Going back up any number of levels is fine. - Choose the level by structure, not by size. If the design wants a small
h2, style it smaller with CSS. Never pickh4because it looks right. - Text that only looks like a heading is not one. A bold
divreading "Shipping address" is invisible to heading navigation. - Don't use headings for styling. A big promo line that doesn't start a section should not be an
h2.
The 2026 WebAIM Million found skipped heading levels on 41.8% of home pages and more than one h1 on 18.1%. Both make the outline harder to trust.
Try the outline builder below. Change heading levels and watch how the outline a screen reader user would get changes, and which choices it flags.
Each line of the page below can be a heading of any level, or bold text that only looks like one.
What pressing H visits
h1 Order history
h2 Open orders
h2 Past orders
h4 October 2026
h3 Contact us
- “October 2026” jumps from h2 to h4, skipping a level.
- “Need help?” is styled text, not a heading. Pressing H skips it.
In a single page app, the page changes without a full load, so two jobs fall to you: update document.title and the h1 for the new view, and decide where focus goes. Moving focus after navigation is covered in the Focus management lesson.
Check your understanding · Exercise 4
Which of these are correct? Select all that apply.
Lists, tables, and forms
Landmarks and headings give the page its shape. Lists, tables, and forms give structure to the content inside. Each one tells the user something they would otherwise have to work out item by item.
Lists: "list, 12 items"
When a screen reader enters a ul or ol, it says how many items there are, and it lets the user skip past the whole list in one key press. That count is useful: "12 results" tells the user how long the next part is before they commit to it. Use lists for navigation links, search results, product cards, and steps. Use ol when order matters, and dl with dt and dd for term and value pairs like an order summary.
One known trap: Safari has long removed list semantics from a ul styled with list-style: none when it is outside a nav, on the reasoning that a list with no bullets may not be a real list. If the count matters, add role="list" to the ul. This is one of the few cases where adding a role that matches the element is a reasonable fix.
Tables: headers for every cell
A real data table lets a screen reader user move cell by cell and hear the matching headers each time: "Amount, 120 dollars" as they move along a row, "Invoice 1042" as they move down a column. A grid of divs reads as a long run of loose text, and the user has to remember what each column was.
<table>
<caption>Unpaid invoices, October 2026</caption>
<thead>
<tr>
<th scope="col">Invoice</th>
<th scope="col">Customer</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><a href="/invoices/1042">1042</a></th>
<td>Acme Ltd</td>
<td>$120.00</td>
</tr>
</tbody>
</table>
caption names the table, th marks headers, and scope says whether a header belongs to a column or a row.captiongives the table its accessible name, so a user moving by tables hears "Unpaid invoices, October 2026, table, 3 columns, 2 rows".thwithscopelinks headers to cells. For simple tables browsers can guess, butscope="col"andscope="row"remove the guessing.- Tables are for data, not layout. Use CSS grid or flexbox to lay out a page. A layout table makes a screen reader announce rows and columns that mean nothing.
- Watch the CSS. Changing the
displayof table elements, for example to make a table scroll or stack on phones, has dropped table semantics in some browsers. Check the accessibility tree after such changes.
Forms: every field needs a name
Forms are where native HTML pays off most, and where the WebAIM Million keeps finding the same bug: in 2026, a third (33.1%) of all form inputs on home pages were not properly labeled. An input with no name at all is announced as something like "edit text", and the user has to guess what to type.
for and id pair is what ties the label to the field; break it and the field is announced with no name.labelwithforpointing at the input'sid, or alabelwrapping the input. It names the field, and clicking the label focuses the field, which gives a bigger target on touch screens.- A placeholder is not a label. Browsers fall back to it for the name when there is no label or
title, but it disappears as soon as the user types and is often low contrast. Keep a visible label. fieldsetandlegendgroup related controls and name the group. A set of radio buttons for "Delivery speed" without them gives the user "Standard, radio button, 1 of 3" with no question.- The right
typeandautocomplete.type="email"shows the email keyboard on phones, andautocomplete="email"or"street-address"lets the browser and password managers fill fields, which helps everyone and especially people with motor or memory impairments. requiredand native validation give a state screen readers announce. Custom error messages need to be linked to their field; the Screen readers lesson shows how witharia-describedbyand live regions.
Check your understanding · Exercise 5
A sign-up form has inputs with only a placeholder ("Email", "Password") and a "Plan" choice built as three radio buttons in a div. What should change? Select all that apply.
The first rule of ARIA
ARIA (Accessible Rich Internet Applications) is a W3C standard: a set of attributes, role and aria-*, that change what the accessibility tree says about an element. It exists for the gaps in HTML, like tabs, comboboxes, and tree views, which have no native element. It is a precise tool, and it is very easy to misuse.
The rules of ARIA use
The W3C note "Using ARIA" sets out five rules. The first one is the one everybody quotes.
- If a native HTML element or attribute already has the meaning and behavior you need, use it instead of adding a role to something else.
- Don't change native semantics unless you really have to. Don't put
role="button"on anh2; put abuttoninside theh2. - Every interactive ARIA control must work with the keyboard. If you say
role="button", you must makeEnterandSpacework. - Never hide a focusable element with
aria-hidden="true"orrole="presentation". The user can tab to it and hear nothing. - Every interactive element needs an accessible name.
The WAI-ARIA Authoring Practices Guide (APG) puts the same idea more bluntly in its "Read me first" page: no ARIA is better than bad ARIA. A wrong role is worse than no role, because it makes a promise to the user that the page then breaks.
ARIA changes what is said, not what happens
This is the most important fact about ARIA. Adding role="button" to a div changes one thing: the screen reader now says "button". It does not make the div focusable, it does not make Enter or Space work, and it does not add a disabled state. You have announced a button and delivered a piece of text, so when the user tries to press it, nothing happens.
The same is true for every ARIA attribute. aria-expanded="true" does not expand anything; your code has to keep it in sync with what is on screen. aria-disabled="true" does not block clicks. role="menu" tells the user to expect arrow keys, Escape, and a specific focus pattern, and if you don't build them, the menu is harder to use than a plain list of links.
What the data says
The 2026 WebAIM Million counted over 133 ARIA attributes per home page on average, more than six times the 2019 level, and 82.7% of home pages used ARIA. Pages with ARIA had 59.1 detected errors on average against 42 on pages without it. Of the 5.7% of pages with an ARIA menu (role="menu"), 22% of those menus were missing the markup and keyboard behavior the role requires.
Be careful with the conclusion. This is a correlation: pages with lots of ARIA also tend to be larger and more complex, which brings more errors anyway. WebAIM says so itself. But it does show that adding ARIA is not the same as adding accessibility, and that a lot of real-world ARIA is written without the behavior it promises.
When ARIA is the right tool
- A state HTML has no attribute for:
aria-expandedon a disclosure button,aria-pressedon a toggle button,aria-current="page"on the link to the current page,aria-invalidon a field with an error. - A name that has no visible source:
aria-labelon an icon-only button,aria-labelledbypointing asectionat its heading so it becomes a region. - A relationship:
aria-describedbylinking a field to its hint or error,aria-controlsfrom a button to the panel it opens. - Announcing changes: live regions with
aria-livefor a toast or a search result count. - A pattern with no element: tabs, combobox, tree, grid with cell navigation. Follow the APG pattern exactly; the ARIA widget patterns lesson covers them, and the Modal dialog and Autocomplete guides show two of them inside full designs.
Decision The design calls for an on/off switch for "Email notifications". How do you build it?
What we pick: Start from the native checkbox: it brings the state, the keyboard behavior, the label, and the form value. Adding role="switch" on top is a well-supported refinement that only changes the announcement. Use a button with aria-pressed when the toggle is an action outside a form, like "Mute". Avoid the bare div.
Check your understanding · Exercise 6
A developer adds role="button" to a clickable div. What has changed? Select all that apply.
Where it breaks in real codebases, and how to catch it
Few teams decide to skip semantic HTML. It erodes, one shortcut at a time, usually through shared components and CSS. These are the patterns that show up again and again in production code.
The usual suspects
| Pattern | What the user gets | Fix |
|---|---|---|
A design system Button that renders a div | Every button in the product is unreachable by keyboard | Fix it once in the component: render a button, forward type, disabled, and ARIA props |
outline: none on all elements | Keyboard users can't see where focus is | Style :focus-visible instead of removing it |
display: none to hide text meant for screen readers | The text is removed from the accessibility tree too | Use a visually hidden class (often called sr-only) that keeps it in the tree |
aria-hidden="true" on a wrapper that contains links | Users tab onto links that announce nothing | Use the inert attribute, which hides and disables focus together |
Positive tabindex values like tabindex="5" | Focus jumps around the page in a strange order | Use only 0 and -1; fix the order in the DOM |
Duplicate ids from a repeated component | Labels and aria-describedby point at the wrong field | Generate ids, for example with React useId |
| Rich text from a CMS with bold paragraphs as headings | No headings to navigate by | Map CMS heading blocks to real h2 and h3 elements |
A lot of these come from the same root: a visual decision made without checking the tree. Hiding, reordering, and restyling all have meaning, so each one deserves a look at the tree.
How you notice
- Lint.
eslint-plugin-jsx-a11yflags clicks on static elements,awith nohref, images with noalt, and labels with no control, as you type. - Tests that query by role. Testing Library recommends
getByRole("button", { name: "Save" })as the first query to reach for. If someone turns the button into adiv, the test fails. Your tests start guarding semantics for free. - Automated scans. axe-core in CI or Lighthouse catches missing names, missing labels, and broken ARIA. These tools catch only part of the problems; they can't tell if a name is helpful or if a heading order makes sense.
- A two-minute keyboard pass. Put the mouse away and
Tabthrough the page. Can you reach everything, see focus, and use each control? This finds most of what tools miss.
The Testing accessibility lesson goes deep on all four, including where screen reader testing fits.
In real products
- GOV.UK Design System builds its components on native elements and tests them with assistive technology. Its "Details" component is the native
detailselement with styling. - React Aria (from Adobe) and Radix UI are headless (unstyled) component libraries that render a native
buttonfor button-like parts and add ARIA only for the patterns HTML lacks, like tabs, menus, and comboboxes. - Framework link components such as Next.js
Linkand React RouterLinkrender a reala href, so new tab, copy link, and the links list keep working while the framework handles the navigation. - Browser-native widgets keep growing.
dialog, thepopoverattribute, andsearchhave arrived in all major browsers in recent years. Each one replaces a pattern teams used to build fromdivs.
Check your understanding · Exercise 7
Your team wants to stop semantic regressions from shipping. Which steps catch them early? Select all that apply.
Using it in a system design interview
Accessibility rarely gets its own question in a frontend system design interview. It shows up as a signal inside every other question: a dropdown, a feed, a checkout, a dashboard. Semantic HTML is the cheapest way to show that signal, because it costs one sentence and changes how the interviewer reads the rest of your design.
When to bring it up
- In the requirements, as a non-functional requirement: "keyboard and screen reader support, WCAG 2.2 AA".
- When you draw the component tree, name the elements: the feed is a
mainwith aulofarticles, each with anh2link; the filter panel is anasidewith aform. - When you design an interactive component, say which native element you start from and where you need ARIA because HTML has no element.
- When you discuss the design system, say that base components render native elements and that tests query by role.
What to say, by level
| Level | What a strong answer includes |
|---|---|
| Junior | Uses button for actions and a href for navigation, labels every input, uses headings in order |
| Mid-level | Explains the accessibility tree, landmarks, and why native elements beat ARIA; names fieldset and legend, table headers, the button versus link rule in a single page app |
| Senior | Makes semantics a property of the system: design system components that render native elements, lint and role-based tests in CI, ARIA patterns from the APG for custom widgets, route changes that update title, heading, and focus |
In the Autocomplete guide, for example, the strong move is to say the input is a real input with a label, the suggestions use the APG combobox pattern because HTML has no element for it, and each suggestion is announced with its position. In the Modal dialog guide, it is to start from the native dialog element with showModal() and explain what it gives you before adding anything.
Check your understanding · Exercise 8
You are designing a product listing page in an interview. Which statements show strong semantic thinking? Select all that apply.
Summary
- Every element means something, and the browser turns that meaning into behavior (focus, keys, disabled) and into the accessibility tree that assistive technology reads.
- The accessibility tree gives each node a role, a name, states, and a value. Inspect it in DevTools when something seems wrong.
- A native
buttongives focus,EnterandSpace, a role,disabled, and form submission for free. Adivgives none of them. - A link goes somewhere, a button does something. Choose by behavior, then style it any way you like.
- Landmarks (
header,nav,main,aside,footer) and an ordered heading outline with oneh1are the map screen reader users navigate by. - Lists announce their size, tables need
caption,th, andscope, and every form field needs a connectedlabel; groups needfieldsetandlegend. - The first rule of ARIA: use native HTML when it exists. ARIA changes what is announced, never what happens, so no ARIA is better than bad ARIA.
- Guard semantics in the system: native elements in base components, role-based tests, lint, and a keyboard pass.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
Why is a div with a click handler not a button, even if it looks exactly like one?
Because a button is more than its looks. A native button is focusable, fires click on Enter and Space, exposes the button role and a name to the accessibility tree, supports disabled, and submits forms. A div has none of that: it is generic, not focusable, and invisible to keyboard and voice control users.
You can rebuild it with role, tabindex, and key handlers, but that is more code, easy to get subtly wrong, and still misses things like forced colors and form submission. The native element is one line and the browser keeps it correct.
Question 2
What is the accessibility tree and how does the browser build it?
It is a simplified copy of the DOM made for assistive technology. The browser walks the DOM, drops hidden content (display: none, hidden, aria-hidden), maps each element to its implicit role, flattens generic divs and spans, computes accessible names (from aria-labelledby, aria-label, labels, alt, content, then title), and applies ARIA overrides.
Each node carries a role, a name, states, and a value. The result goes to the operating system's accessibility API, which screen readers and voice control read. You can inspect it in the DevTools Accessibility pane.
Question 3
How do you decide between a button and a link?
A link goes somewhere; a button does something. If the result has its own URL that someone could bookmark, share, or open in a new tab, it is an a href. If it changes the current page or server state, like opening a menu, submitting, or deleting, it is a button.
Looks don't decide it: a "Sign up" call to action that navigates is a link styled like a button. In a single page app, use the framework's link component, which renders a real a href, rather than a button that calls the router.
Question 4
What is the first rule of ARIA, and why does it exist?
If a native element or attribute already has the meaning and behavior you need, use it instead of adding ARIA. The reason is that ARIA only changes what assistive technology is told; it never adds behavior. role="button" doesn't make anything focusable or keyboard operable.
Bad ARIA makes promises the page doesn't keep, which is worse than no ARIA. The 2026 WebAIM Million found pages with ARIA averaged 59.1 detected errors against 42 without, and 22% of ARIA menus lacked the required markup and behavior. It is a correlation, but it shows ARIA is not accessibility by itself.
Question 5
How would you make sure a large app stays semantic as many teams work on it?
Put the semantics in the system, not in each screen. Base components in the design system render native elements (button, a, input with label, dialog). Lint with eslint-plugin-jsx-a11y, write tests that query by role and name so regressions fail, and run axe in CI.
Add a short manual keyboard pass for new features, because tools catch only part of the issues. For custom widgets with no native element, follow the APG pattern exactly and own the keyboard and state work it requires.
Practice it
These design problems lean on semantic html first.
References
- MDN: HTML elements reference Every element with its implicit role and accessibility notes.
- W3C: Using ARIA (the rules of ARIA use) The five rules, starting with the first rule of ARIA.
- WAI-ARIA Authoring Practices Guide: Read me first Where "no ARIA is better than bad ARIA" comes from.
- W3C: HTML Accessibility API Mappings (HTML-AAM) How each HTML element maps to roles in the accessibility tree.
- WebAIM: The WebAIM Million The yearly scan of the top million home pages; the 2026 numbers used here.
- web.dev: Learn Accessibility A free course covering semantics, landmarks, headings, and ARIA.
- MDN: Accessibility tree A short definition of the tree and what each node holds.
Read next
- Keyboard navigationMaking every action work without a mouse: a Tab order that follows the page, a focus ring you can always see, arrow keys inside composite widgets, skip links past repeated content, and no place where focus gets stuck.
- Focus managementDeciding where keyboard focus goes when the page changes under the user (route changes, modals, deletes, new content) so keyboard and screen reader users never lose their place.
- ARIA widget patternsThe tested recipes for custom widgets from the ARIA Authoring Practices Guide: combobox, listbox, menu, tabs, dialog, tree, grid, disclosure, and accordion, with the roles, states, and keys each one promises, and how to pick the right one.
- Screen readers and live regionsHow screen readers turn a page into speech or braille, how every control gets its accessible name and description, and how to announce changes like search results, saves, and errors with live regions.