Semantic HTML for Accessibility
Semantic HTML means choosing the element that says what the content is: <h2> for a section heading, <nav> for the site menu, <ul> for a list of options, <button> for an action, <label> for the name of a field, <th> for a table header. From these elements the browser builds an accessibility tree of roles, names and states, and that tree, not the pixels on screen, is what a screen reader speaks and what voice control listens for. The same native elements also decide what the Tab key can reach.
The lesson HTML Basics for Testers introduced the elements themselves. This lesson looks at them from the other side: what each one gives assistive technology, what is lost when a page is built from <div> and <span> alone, and how you check the difference on a real page before any keyboard or screen reader session.
Why it matters
A page built from styled <div> elements can look finished and still be unusable: a screen reader user hears "clickable" instead of "button", opens an empty list of headings, lands in a field with no name and reads a table number with no column. Each of these is a failure of WCAG 2.2 success criterion 1.3.1 Info and Relationships or 4.1.2 Name, Role, Value, both Level A, so the page fails conformance at the lowest level and the people who depend on assistive technology cannot complete the task.
For the tester, semantics are among the cheapest accessibility checks there are. You do not need a screen reader to see that a heading is a styled <div> or that an input has no <label>: the markup and the browser’s accessibility panel show it in seconds, and the fix is usually a one-element change. Finding it in the first review of a new component saves a full screen reader round later, and the same roles and names make automated tests and locators more stable too.
From markup to the accessibility tree
When the browser parses HTML it builds the DOM, the live tree of the page’s elements, and from the DOM a second structure: the accessibility tree. Every meaningful element becomes a node with a role (heading, link, button, textbox, cell), an accessible name (the heading text, the link text, the label of a field) and states or properties (heading level 2, required, checked, disabled). Elements that carry no meaning, such as <div> and <span>, become generic nodes or are left out of the tree entirely.
Screen readers and voice control read this tree through the operating system’s accessibility API, not the rendered pixels. A screen reader announces role and name together ("Place order, button"), offers lists of headings, landmarks and links so the user can jump instead of reading in order, and uses the role to decide which keys operate the element. Voice control activates whatever the user names ("click Place order"), so the spoken name must exist in the tree. Keyboard users reach only what is focusable, which native controls are by default. Styling cannot add a role: CSS can hide content from the tree, but a <div> styled as a button is still a generic node in it.
- Role: what kind of thing it is. It comes from the element (
<button>,<h2>,<a href>), or as a last resort from an ARIA role attribute, which a later lesson covers. - Name: what it is called. The browser computes it from the content, a
<label>, analt, a<caption>or ARIA attributes, in the fixed order of the accessible name computation. - State and value: what it is doing now. Native elements keep these up to date for free: a checked
<input type="checkbox">, a disabled<button>, the text typed into a<textarea>.
Headings, landmarks and lists: the structure users navigate by
Screen reader users rarely read a page from top to bottom. They open the headings list to see what the page contains, jump to the main landmark to skip the menu, and hear "list, four items" before a list so they know how much is coming. Each of those moves depends on one element having been used, and each has a precise check.
- Headings,
<h1>to<h6>: one<h1>for the page topic,<h2>for its sections,<h3>inside them. A visual heading made of bold text in a<div>is missing from the headings list and fails 1.3.1; heading text that does not describe its section fails 2.4.6 Headings and Labels (Level AA). Skipped levels confuse users but are not a WCAG failure on their own, so report them with lower severity. - Landmarks: at the top level of the page
<header>maps to the banner role,<nav>to navigation,<main>to main,<aside>to complementary and<footer>to contentinfo. A<section>only becomes a region when it has a name, usually fromaria-labelledbypointing at its heading or fromaria-label. Landmarks let users skip the blocks repeated on every page, which is what 2.4.1 Bypass Blocks asks for. Check that the content sits inside<main>and that two<nav>elements carry different names, such as "Main menu" and "Footer links". - Lists,
<ul>and<ol>: a screen reader announces that a list begins and how many items it has. A menu built from<div>rows reads as unrelated lines of text, and an<ol>of checkout steps loses its order when it becomes divs. Styled bullets alone do not create a list.
Buttons, links, labels and tables: name, role and relationships
The second group is where 4.1.2 Name, Role, Value applies: every user interface component must expose its name, role, states and value to assistive technology. Native elements do this by default; replacements built from <div> or <span> do not, and the wrong native element is a defect too.
- Button or link: a
<button>performs an action on this page (submit, open, delete) and is activated with Enter and Space; an<a href>goes somewhere and is activated with Enter. Screen readers announce the two differently and list links separately, so a<div onclick>acting as a button, or a link styled as a button that only runs a script, has a clear expected result: the native element for what it does. An<a>withouthrefis not a link at all: it is not focusable, and its text is read as plain text, not as a link. - Labels: a field’s name comes from a
<label>whoseforequals the field’sid, or from a<label>that wraps the field. Theplaceholderattribute is only a fallback in the name computation and its hint disappears as soon as the user types, so a field whose only label is a placeholder is a defect to report. A group of radio buttons or checkboxes takes its shared question from<fieldset>and<legend>; without them the user hears "Yes, radio button" with no idea what the question was. - Tables:
<table>is for data with rows and columns, with<th scope="col">or<th scope="row">for the headers and a<caption>for the title. The tree then links each data cell to its headers, so as the user moves from cell to cell a screen reader announces the header that changed, for example the column name before each value, and the user can move along a row or a column. A grid of<div>elements, or a table whose headers are bold<td>cells, loses these relationships and fails 1.3.1. The reverse mistake also exists: a<table>used for layout, which can announce meaningless rows and columns.
Worked example: the same checkout page in two markups
A front-end component library renders the checkout page as markup A below: apart from the input itself, every element is a <div> with a class, styled to look like a heading, a menu, a field label, a summary table and a button. The design review passes, because on screen it is indistinguishable from markup B, which uses one semantic element per structure.
Open both in the browser’s DevTools (right-click an element and choose Inspect; in Chrome and Edge the Accessibility tab, in the same row of tabs as Styles, shows the selected element’s role and computed name) and the difference is immediate. In A, "Checkout" is a generic node with text, so the headings list is empty; the menu is two generic items that Tab never reaches; the input has no name; "Total" and "600 SAR" are unrelated text; and "Place order" is a generic node that only a mouse can activate. In B the tree shows heading level 1 "Checkout", a navigation landmark holding a list of two links, a main landmark, a required textbox named "Postal code", a table in which "600 SAR" is linked to its row header "Total", and a button named "Place order" that Tab reaches and Enter or Space activates. Nothing changed visually; everything changed for assistive technology.
<!-- A: div soup -->
<div class="title">Checkout</div>
<div class="menu">
<div class="item" onclick="go('/orders')">Orders</div>
<div class="item" onclick="go('/addresses')">Addresses</div>
</div>
<div class="hint">Postal code</div>
<input class="zip">
<div class="row"><div class="cell">Total</div><div class="cell">600 SAR</div></div>
<div class="btn" onclick="pay()">Place order</div>
<!-- B: semantic HTML, identical on screen -->
<nav>
<ul>
<li><a href="/orders">Orders</a></li>
<li><a href="/addresses">Addresses</a></li>
</ul>
</nav>
<main>
<h1>Checkout</h1>
<form action="/orders" method="post">
<label for="zip">Postal code</label>
<input id="zip" name="zip" required>
<table>
<caption>Order summary</caption>
<tr><th scope="row">Total</th><td>600 SAR</td></tr>
</table>
<button type="submit">Place order</button>
</form>
</main>A: div soupLooks right
Apart from the input, every structure is a styled <div> with a class
- Headings list:
- Empty: "Checkout" is generic textFail
- Landmarks:
- None: only generic divsFail
- Menu (Orders, Addresses):
- Two lines of text; Tab never reaches themFail
- Postal code field:
- Textbox with no nameFail
- Order total:
- "Total" and "600 SAR" as unrelated textFail
- "Place order" control:
- Generic node; mouse onlyFail
B: semantic HTMLReads right too
One native element per structure, identical on screen
- Headings list:
- Heading level 1 "Checkout"Pass
- Landmarks:
- navigation and mainPass
- Menu (Orders, Addresses):
- List of two links, each focusablePass
- Postal code field:
- Textbox "Postal code", requiredPass
- Order total:
- Cell "600 SAR" linked to its row header "Total"Pass
- "Place order" control:
- Button; Tab reaches it, Enter or Space activates itPass
Each row describes the accessibility tree as the browser’s DevTools accessibility panel shows it.
Common mistakes
- Judging accessibility from the screen. A page that looks right can have an almost empty accessibility tree; check the markup or the accessibility panel, not the pixels.
- Treating everything clickable the same: a
<div onclick>is not a button, an<a>withouthrefis not a link, and a<button>that navigates to another page should be a link. - Accepting a
placeholderas a label, or a<label>whoseforpoints to anidthat does not exist on the page. - Checking how headings look instead of what they are: a page can have six large bold lines and no
<h1>at all. - Reporting "not accessible to screen readers" without the role and name: a useful defect states what the tree exposes, what it should expose and the success criterion it fails.
- Reaching for ARIA to repair a
<div>when a native element would give the role, name, keyboard behaviour and states in one change.
The QA perspective
On a team, semantics are checked earliest in review. When a new component or page template appears in a pull request or a design-system update, read its markup before any test session: real headings, a <button> for every action, a <label> for every field, <th> in data tables. Agree these expectations with the front-end developer and write them into the definition of done, so the check happens in code review rather than as a finding weeks later.
When you test a built page, open the DevTools accessibility panel to read the role and computed name of each control, press Tab through the page to confirm every control is reachable, and compare the visual headings with the headings in the tree. Record each defect with the element as rendered, the role and name the tree exposes, the expected element and the success criterion, usually 1.3.1 or 4.1.2. The keyboard and screen reader guides that follow build on exactly this layer: when the semantics are wrong, every later test fails for the same root cause.
Practice exercise
Review the markup of a product card
A product listing shows cards with the product name in large text, a star rating, a price, an "Add to cart" control and a "Details" control that opens the product page. The developer’s markup for one card is below. Without a screen reader, write down what the accessibility tree will expose for each part, name the defects with the WCAG 2.2 criterion each fails, and give the semantic element you expect instead.
Then write one test case for the corrected card that a tester with only a keyboard could execute.
<div class="card">
<div class="name">Wireless keyboard</div>
<div class="rating">★★★★☆</div>
<div class="price">249 SAR</div>
<span class="btn" onclick="addToCart(42)">Add to cart</span>
<a class="btn" onclick="openProduct(42)">Details</a>
</div>- Go through the card from top to bottom and write the role and name each element will have in the accessibility tree.
- Mark every place where the role is generic or the name is missing or misleading.
- Choose the native element that gives the right role, name and keyboard behaviour for each one.
- Write the keyboard test case with steps and an expected result for each step.
Show the model answer
Tree today: five generic nodes with text. The product name is not a heading, so the card cannot be found from the headings list (1.3.1). The rating conveys its value only through star symbols; whether the screen reader reads "black star" four times and "white star" once, or skips the symbols, the user has to work it out, so a text equivalent such as "4 of 5 stars" is needed (1.1.1 Non-text Content: WCAG counts a pattern of characters that does not express human language, like these stars, as non-text content, just like an icon or image). "Add to cart" is a <span> with no role that is not focusable, so a keyboard user cannot add the product (4.1.2 and 2.1.1 Keyboard). "Details" is an <a> without href, so it is not a link, not focusable and absent from the links list (4.1.2 and 2.1.1).
Expected markup: each card an <li> in a <ul> of products, the name as a heading at the right level (for example <h3> under an <h2> "Results"), the text equivalent "4 of 5 stars" (for example an <img> of the stars with alt="4 of 5 stars", or visible text with the star symbols removed from the accessibility tree by aria-hidden="true"), <button type="button">Add to cart</button>, and <a href="/products/42">Details</a>. Since every card repeats the same control names, write down a follow-up question: how will a screen reader user know which product "Add to cart" belongs to? The screen reader testing guide answers it in its section "Checking names, labels and descriptions in practice".
Keyboard test case. Precondition: the product list is open with at least one card. Step 1: press Tab from the start of the page until focus reaches the first card; expected: a visible focus ring on "Add to cart". Step 2: press Enter; expected: the cart count increases by one. Step 3: press Space; expected: the cart count increases by one again, because a button responds to both keys. Step 4: press Tab; expected: focus moves to "Details". Step 5: press Enter; expected: the product page for the wireless keyboard opens.
Frequently asked questions
Does semantic HTML make a page WCAG-conformant?
No. It covers a large part of 1.3.1 and 4.1.2 and gives keyboard operability for free, but contrast, text alternatives, visible focus, error messages and many other criteria remain. Semantics are the foundation the other checks assume.
Can I test semantics without a screen reader?
Most of it, yes. The DevTools accessibility panel shows the role and computed name of any element, the Tab key shows what is focusable, and a headings outline shows the structure. A screen reader session, which a later guide covers, confirms what users actually hear and catches what the tree alone cannot show.
If a <div> has role="button", is it as good as a <button>?
Only once a developer has added focusability, Enter and Space handling and the disabled state by hand, and keeps all of it working. The native element has this built in and tested in every browser, which is why the first rule of ARIA use says to prefer it. The ARIA lesson covers when a role is the right choice.
References
- Web Content Accessibility Guidelines (WCAG) 2.2The W3C Recommendation that defines the testable success criteria for accessible web content, organised under four principles and three conformance levels (A, AA, AAA).
- HTML Living StandardThe HTML language, including named character references (entities) and the parsing rules browsers follow.
- Accessible Name and Description Computation 1.2How browsers compute the accessible name and description of an element from labels, ARIA attributes and content (a W3C Working Draft).
- Understanding WCAG 2.2The informative explanation of each WCAG 2.2 guideline and success criterion: intent, who benefits, examples and techniques (not normative).
Practise with Mutqan tools
Beautify HTML with proper indentation of nested elements, preserving pre, script and style blocks.
Generate structured test cases (id, title, preconditions, steps, expected result, priority) from a feature description and field list.
Generate a tailored QA checklist (functional, UI, API, security, performance, accessibility, release) for any feature with Mutqan AI.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.