cf.completefrontend
My progress
FUNDAMENTALS · MODULE 12

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.

Two copies of a Place order control that look the same. On the left, a div with a click handler: a mouse click works, but Tab never reaches it, Enter and Space do nothing, the accessibility tree shows plain text, and a voice command finds no match. On the right, a button: the mouse click works, Tab reaches it with a focus ring, Enter and Space click it, the tree shows a named button, and the voice command works.div with onClickPlace ordermouse click: worksTab: never reachedEnter or Space: nothingtree: plain textvoice command: no matchbuttonPlace ordermouse click: worksTab: reached, ring shownEnter or Space: clicktree: button, namedvoice command: works
Both pass a quick mouse test, which is why the div version ships. Only one of them works for the other four ways people use a page.

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.

On the left, a DOM tree: body, then a div with class wrapper, holding a nav with a link to Home, a div with a click handler reading Save, an h2 reading Orders, and a hidden span reading Beta. On the right, the accessibility tree built from it: a document holding a navigation landmark with a link named Home, plain text Save, and a level 2 heading named Orders. The wrapper div was flattened away and the hidden span was dropped.DOMAccessibility treebodydiv.wrappernava href: Homediv onclick: Saveh2: Ordersspan hidden: Betadocumentnavigationlink “Home”text “Save”heading 2 “Orders”wrapper flattened awayhidden span dropped
The tree keeps meaning and drops the rest. The wrapper disappears, the hidden span is gone, and the clickable div survives only as text: nothing in the tree says it can be pressed.

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, an alt, 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.

  1. Hidden things are dropped. Elements with display: none, visibility: hidden, the hidden attribute, or aria-hidden="true" do not appear, and neither do their children.
  2. Each element gets its implicit role. button becomes button, a href becomes link, h2 becomes heading with level 2, ul becomes list, nav becomes navigation.
  3. Meaningless wrappers are flattened. A div or span has the role generic. Browsers often skip generic nodes and pass their children up, so ten nested divs leave almost no trace.
  4. Names are computed. For each node that needs one, the browser runs the accessible name algorithm: aria-labelledby first, then aria-label, then the native source (a label, alt, caption, legend), then the text inside, then title. For a text input, placeholder comes last of all.
  5. ARIA overrides are applied. A role or aria-* attribute replaces or adds to what the HTML gave. This is powerful and dangerous, as chapter 7 shows.
  6. 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.
A stack of five layers, top to bottom. Your code: HTML elements like button, nav, h2, and label, then ARIA attributes like role and aria attributes, which override them. The browser: the accessibility tree with role, name, state, and value. The operating system: a platform accessibility API, UI Automation on Windows, NSAccessibility on macOS, or AT-SPI on Linux. The user: assistive technology such as a screen reader, voice control, or a switch device.HTML elementsbutton, nav, h2, labelyour codeARIA attributesrole, aria-*: overridesyour codeAccessibility treerole, name, state, valuethe browserPlatform APIUIA, NSAccessibility, AT-SPIthe OSAssistive technologyscreen reader, voice, switchthe usermeaningspeech
Each layer only sees the one above it. A screen reader never reads your CSS or your click handlers: if the meaning is not in the tree, it does not reach the user.

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?

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. Tab reaches it, Shift+Tab goes back, and the browser draws a focus ring when the user is on the keyboard (the :focus-visible state).
  • Keys. Enter fires click as soon as the key goes down. Space fires click when 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 firing click, and is announced as unavailable or dimmed.
  • Forms. Inside a form, a button submits it, and pressing Enter in 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.

Element:
What the browser gives this element without any extra code
Role in the accessibility treebutton
Reached with TabYes
Enter activates itYes
Space activates itYes, when the key is released
Can be disabledYes, 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.

A sequence diagram with four lifelines: User, Browser, onClick, and Screen reader. The user presses Tab and the browser tells the screen reader that focus is on Save, button. The user presses Space down: the browser shows the button as active but fires nothing yet. The user releases Space: the browser fires a click event with detail 0 to the onClick handler. The handler updates the page to say Saved, and the browser reports the change to the screen reader.UserBrowseronClickScreen readerTabSave, buttonSpace downSpace upclick, detail 0render: Savedchange reported
Every arrow from the browser is free with a 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.

tsx
// 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>
Fifteen lines that are easy to get wrong against one line that the browser keeps correct.

It is not only buttons

Many native elements carry a whole interaction pattern with them. Before building a custom widget, check this table.

ElementWhat you get for freeCommon way to lose it
a hrefLink role, focus, Enter to follow, open in new tab, copy link, history, prefetch by the browserLeaving out href, or using onClick with no href
input, textareaText editing, selection, undo, autofill, spell check, the right mobile keyboard for type="email" or inputmodeA div with contenteditable for a simple field
input type="checkbox", radioChecked state, Space to toggle, arrow keys move between radios in a groupStyled spans with an icon
selectA listbox that works with keyboard, screen readers, and the native picker on phonesA custom dropdown built from divs
details and summaryExpand and collapse, the expanded state announced, Enter and Space to toggleA 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 layerA positioned div with a backdrop
popover attributeDrawn 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.

A matrix of four ways to build a clickable control against six properties: role, reached by Tab, Enter, Space, disabled, and form submit. A button has all six. A link with href has a role, Tab, and Enter; Space, disabled, and submit do not apply to links. A div with onClick has none of them. A div with role button and tabindex 0 has a role and Tab, but Enter, Space, and disabled must be built by hand, and it can never submit a form.roleTabEnterSpacedisabledsubmitbuttona hrefdiv onClickdiv role=buttonyesyesyesyesyesyesyesyesyesn/an/an/anonononononoyesyesDIYDIYDIYno
DIY means do it yourself: a key handler or an 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.

Markup:

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.

Keys:

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.

ElementImplicit roleWhen it counts as a landmark
headerbannerOnly when it is not inside article, aside, main, nav, or section. Inside those it is not a landmark.
navnavigationAlways. Label it when there is more than one.
mainmainAlways. Exactly one visible main per page.
asidecomplementaryAt 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.
footercontentinfoSame rule as header: not a landmark inside article, aside, main, nav, or section.
sectionregionOnly when it has an accessible name, for example from aria-labelledby pointing at its heading.
formformOnly when it has an accessible name.
searchsearchAlways. A newer element that wraps a search form; supported in all major browsers.
A shop page drawn as a browser window split into landmarks. The header across the top is the banner, and inside it sit a nav labelled Primary, which is navigation, and a search element, which is search. Below, the main element in the middle is main, and the aside on the right is complementary. Across the bottom, the footer is contentinfo, with a second nav labelled Footer inside it.shop.example.comheaderbannernav “Primary”navigationsearchsearchmainmainasidecomplementaryfootercontentinfonav “Footer”navigation
Each element brings its role with it, so you never write 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 h1 that names the page. Usually the same idea as the title, like "Invoice 1042". Not the site logo on every page.
  • Don't skip levels going down. After an h2, the next deeper heading is h3, not h4. 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 pick h4 because it looks right.
  • Text that only looks like a heading is not one. A bold div reading "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.

html
<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>
A caption names the table, th marks headers, and scope says whether a header belongs to a column or a row.
  • caption gives the table its accessible name, so a user moving by tables hears "Unpaid invoices, October 2026, table, 3 columns, 2 rows".
  • th with scope links headers to cells. For simple tables browsers can guess, but scope="col" and scope="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 display of 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.

A checkout form written out line by line with notes. A label with for equal to email names the field. An input with id email and type email shows the email keyboard on phones, and autocomplete email lets the browser fill it. A fieldset groups the radio buttons and its legend, Delivery, names the group. Each radio inside has its own label and is announced like Standard, radio, 1 of 2. A button reading Place order submits the form, and pressing Enter in a field does too.<label for="email">Email</label>names the field<input id="email" type="email"email keyboard on phones autocomplete="email">browser can fill it<fieldset>groups the radios <legend>Delivery</legend>names the group <label><input type="radio"“Standard, radio, 1 of 2”</fieldset>end of the group<button>Place order</button>submits; Enter does too
Every note on the right comes from the markup on the left, with no script. The for and id pair is what ties the label to the field; break it and the field is announced with no name.
  • label with for pointing at the input's id, or a label wrapping 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.
  • fieldset and legend group 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 type and autocomplete. type="email" shows the email keyboard on phones, and autocomplete="email" or "street-address" lets the browser and password managers fill fields, which helps everyone and especially people with motor or memory impairments.
  • required and native validation give a state screen readers announce. Custom error messages need to be linked to their field; the Screen readers lesson shows how with aria-describedby and 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.

  1. 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.
  2. Don't change native semantics unless you really have to. Don't put role="button" on an h2; put a button inside the h2.
  3. Every interactive ARIA control must work with the keyboard. If you say role="button", you must make Enter and Space work.
  4. Never hide a focusable element with aria-hidden="true" or role="presentation". The user can tab to it and hear nothing.
  5. 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.

Two bar charts from the 2026 WebAIM Million, drawn to scale. Top: average detected errors per home page. Pages without ARIA averaged 42 errors; pages with ARIA averaged 59.1. Bottom: ARIA attributes per home page grew from about 22 in 2019 to over 133 in 2026, more than six times as many.average detected errors per home page, 2026home pages without ARIA42 errorshome pages with ARIA59.1 errorsARIA attributes per home page2019about 222026over 133
More ARIA has not meant fewer errors. Busier pages explain part of the gap, but the numbers show that ARIA on its own does not make a page accessible.

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-expanded on a disclosure button, aria-pressed on a toggle button, aria-current="page" on the link to the current page, aria-invalid on a field with an error.
  • A name that has no visible source: aria-label on an icon-only button, aria-labelledby pointing a section at its heading so it becomes a region.
  • A relationship: aria-describedby linking a field to its hint or error, aria-controls from a button to the panel it opens.
  • Announcing changes: live regions with aria-live for 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?

A div with role="switch"

Add role="switch", aria-checked, tabIndex=0

Handle Space and Enter and clicks yourself

Pros

  • Full control of the markup

Cons

  • You rebuild focus, keys, and state by hand
  • No form value, no label click
  • Easy to let aria-checked drift out of sync

A button with aria-pressed

button type="button" aria-pressed="true"

Toggle the attribute on click

Pros

  • Focus and keys are free
  • Announced as a toggle button, pressed or not pressed

Cons

  • Not part of form data
  • Needs one attribute kept in sync

A native checkbox, styled as a switch

input type="checkbox" with a connected label

Optionally add role="switch" so it is announced as on or off

Style it with CSS

Pros

  • Focus, Space, checked state, label click, and form value are all free
  • Works before JavaScript

Cons

  • Styling a checkbox takes some CSS work

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

PatternWhat the user getsFix
A design system Button that renders a divEvery button in the product is unreachable by keyboardFix it once in the component: render a button, forward type, disabled, and ARIA props
outline: none on all elementsKeyboard users can't see where focus isStyle :focus-visible instead of removing it
display: none to hide text meant for screen readersThe text is removed from the accessibility tree tooUse a visually hidden class (often called sr-only) that keeps it in the tree
aria-hidden="true" on a wrapper that contains linksUsers tab onto links that announce nothingUse the inert attribute, which hides and disables focus together
Positive tabindex values like tabindex="5"Focus jumps around the page in a strange orderUse only 0 and -1; fix the order in the DOM
Duplicate ids from a repeated componentLabels and aria-describedby point at the wrong fieldGenerate ids, for example with React useId
Rich text from a CMS with bold paragraphs as headingsNo headings to navigate byMap 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-a11y flags clicks on static elements, a with no href, images with no alt, 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 a div, 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 Tab through 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 details element with styling.
  • React Aria (from Adobe) and Radix UI are headless (unstyled) component libraries that render a native button for button-like parts and add ARIA only for the patterns HTML lacks, like tabs, menus, and comboboxes.
  • Framework link components such as Next.js Link and React Router Link render a real a href, so new tab, copy link, and the links list keep working while the framework handles the navigation.
  • Browser-native widgets keep growing. dialog, the popover attribute, and search have arrived in all major browsers in recent years. Each one replaces a pattern teams used to build from divs.

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 main with a ul of articles, each with an h2 link; the filter panel is an aside with a form.
  • 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

LevelWhat a strong answer includes
JuniorUses button for actions and a href for navigation, labels every input, uses headings in order
Mid-levelExplains 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
SeniorMakes 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 button gives focus, Enter and Space, a role, disabled, and form submission for free. A div gives 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 one h1 are the map screen reader users navigate by.
  • Lists announce their size, tables need caption, th, and scope, and every form field needs a connected label; groups need fieldset and legend.
  • 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?

Question 2

What is the accessibility tree and how does the browser build it?

Question 3

How do you decide between a button and a link?

Question 4

What is the first rule of ARIA, and why does it exist?

Question 5

How would you make sure a large app stays semantic as many teams work on it?

Practice it

These design problems lean on semantic html first.

References

Read next