HTML Accessibility Basics
Accessibility in HTML means writing markup that works for everyone, including people who use screen readers, keyboard-only navigation, voice control, or browser zoom. Because HTML elements carry built-in meaning (called semantics), the browser can expose that meaning to assistive technology automatically — but only if you choose the right elements and attributes. Get this right and accessibility mostly comes for free; get it wrong and you force disabled users to fight your page.
Overview: How Browsers Turn HTML Into an Accessible Experience
When a browser parses your HTML, it doesn’t just build the visual page — it also builds an accessibility tree, a parallel structure derived from the DOM that assistive technologies (screen readers, switch devices, voice control software) read instead of the pixels on screen. Every element in the accessibility tree gets a role (what kind of thing it is: button, link, heading, list), a name (what to call it), and sometimes a state (checked, expanded, disabled).
Semantic HTML elements like <button>, <nav>, <header>, and <h1>–<h6> already have a role built in. A screen reader announces <button>Save</button> as “Save, button” without you doing anything extra, and it lets a user jump directly to the <nav> landmark or list all headings on the page to skim its structure. This is the core lesson of HTML accessibility: choose the element whose native semantics match what the content actually is, rather than styling a generic <div> or <span> to merely look like the real thing. A styled <div> that looks like a button has no role, no keyboard focus, and no way to be “pressed” via keyboard unless you rebuild all of that behavior yourself with JavaScript and ARIA — and even then it’s easy to get wrong.
Accessibility also depends on non-visual information reaching the DOM: the text alternative for an image, the label tied to a form field, the language of the page, and the reading order of the content. None of this requires CSS or JavaScript — it is fundamentally an HTML authoring discipline, which is why it belongs alongside the other global attributes you use every day.
Syntax: The Core Attributes and Elements You’ll Use
Accessible HTML relies on a small set of recurring attributes and elements. There is no single “accessibility tag” — instead, you combine several tools depending on the content:
| Attribute / Element | Purpose |
|---|---|
alt |
Text alternative for an <img>; read aloud by screen readers, shown if the image fails to load. |
<label> + for/id |
Associates visible text with a form control so screen readers announce it and clicking the text focuses the field. |
lang |
Declares the human language of the page or an element, so screen readers use correct pronunciation rules. |
Landmark elements (<header>, <nav>, <main>, <footer>, <aside>) |
Divide the page into regions that assistive tech users can jump between directly. |
aria-label |
Supplies an accessible name for an element when there is no visible text to use (e.g. an icon-only button). |
aria-labelledby |
Points to the id of another element whose text should serve as this element’s accessible name. |
aria-describedby |
Points to an element with extra descriptive text, announced after the name (e.g. a hint or error message). |
aria-expanded, aria-hidden, aria-live |
Communicate dynamic state and updates for widgets like menus, accordions, and live status messages. |
tabindex |
Adjusts keyboard focus order; 0 adds an element to the natural tab order, -1 removes it but allows programmatic focus. |
Examples
Example 1: A Properly Labeled Form
<form>
<label for="full-name">Full name</label>
<input type="text" id="full-name" name="full-name" required>
<label for="email">Email address</label>
<input type="email" id="email" name="email" aria-describedby="email-hint" required>
<p id="email-hint">We'll only use this to send your receipt.</p>
<button type="submit">Create account</button>
</form>
Result: The browser renders a normal-looking form with two text fields and a submit button. What’s invisible visually but critical for accessibility: clicking either label text moves focus into its matching field, and a screen reader announces “Email address, edit text, We’ll only use this to send your receipt” when the field receives focus — because aria-describedby links the hint paragraph to the input.
This works because for="email" on the label matches id="email" on the input, creating a programmatic association the browser exposes in the accessibility tree, not just a visual one.
Example 2: Semantic Landmarks for Page Structure
<header>
<h1>Trailhead Gear Co.</h1>
<nav aria-label="Main">
<ul>
<li><a href="/shop">Shop</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<h2>Featured Products</h2>
<p>Hand-built packs for every trail.</p>
</main>
<footer>
<p>© 2026 Trailhead Gear Co.</p>
</footer>
Result: Visually this renders a normal header with a heading and navigation links, a main content region, and a footer — nothing looks different from unstructured markup. But a screen reader user pressing their landmarks key hears three stops: “banner,” “main,” and “contentinfo,” and can jump straight to the page content while skipping repeated navigation on every page load.
The aria-label="Main" on <nav> matters because a page can have more than one <nav> (for example, a footer nav too); the label lets assistive tech announce which navigation is which instead of two unlabeled “navigation” regions.
Example 3: An Accessible Icon Button and Live Region
<button type="button" aria-label="Close dialog" aria-expanded="false">
×
</button>
<div role="status" aria-live="polite">
Item added to cart.
</div>
Result: Sighted users see a small × symbol button and, elsewhere on the page, a status line reading “Item added to cart.” A screen reader user hears “Close dialog, button, collapsed” when focusing the button — because there’s no visible text to announce, aria-label supplies it. When the status <div>‘s text changes (for example after adding an item via a script), aria-live="polite" tells assistive tech to announce the update automatically, without the user needing to move focus to notice it.
Note that ARIA never changes what’s drawn on screen — it only changes what’s exposed to the accessibility tree. If the visible text and the ARIA label ever disagree, always trust the visible text as the source of truth for what users expect.
How It Works Step by Step: Building the Accessibility Tree
- The browser parses your HTML into the DOM, exactly as it would for any page.
- For every DOM node, the browser computes an implicit ARIA role from the element type (e.g.
<a href="...">becomes role “link,”<input type="checkbox">becomes role “checkbox”). - The browser computes an accessible name for the node, checking sources in priority order:
aria-labelledby, thenaria-label, then an associated<label>, then the element’s own text content, then thetitleattribute as a last resort. - The browser computes any relevant state (checked, disabled, expanded, current) from attributes like
aria-expandedor the nativedisabledattribute. - This role/name/state tree, called the accessibility tree, is exposed through the operating system’s accessibility API, which is what screen readers actually query — they never read your raw HTML.
- Explicit ARIA attributes can override the implicit role or name computed from the element itself, which is powerful but also why misusing ARIA (adding the wrong role, or a label that contradicts the visible text) actively breaks the experience rather than merely doing nothing.
Common Mistakes
Mistake 1: Meaningful Images Without Alt Text
<img src="sales-chart-2024.png">
Without an alt attribute, a screen reader typically falls back to reading the filename, so the user hears something like “sales dash chart dash 2024 dot p n g, image” instead of the actual information the chart conveys.
<img src="sales-chart-2024.png" alt="Bar chart showing sales grew 40% from Q1 to Q4 2024">
The corrected version describes the content and meaning of the image, not just its appearance, so a non-visual user gets the same information a sighted user gets from looking at it.
Mistake 2: Skipping Heading Levels
<h1>Trail Running Guide</h1>
<h3>Choosing Shoes</h3>
Screen reader users frequently navigate by jumping between headings and rely on the levels to understand the document outline. Jumping from <h1> straight to <h3> implies a missing <h2> section and makes the hierarchy confusing, even though it renders with no visible error.
<h1>Trail Running Guide</h1>
<h2>Choosing Shoes</h2>
Heading levels should step down one at a time, reflecting the actual nesting of sections, the same way you’d outline an essay.
Mistake 3: A Div Styled to Look Like a Button
<div class="btn">Submit</div>
A <div> has no role of “button” in the accessibility tree, cannot receive keyboard focus by default, and does not respond to the Enter or Space keys the way a real button does — keyboard-only users simply cannot activate it.
<button type="button" class="btn">Submit</button>
The native <button> element is focusable, keyboard-operable, and announced as a button automatically, with none of that behavior needing to be rebuilt by hand.
Best Practices
- Reach for a semantic element (
<button>,<nav>,<table>,<h2>) before reaching for ARIA on a generic<div>or<span>; ARIA should supplement HTML, not replace it. - Give every meaningful
<img>a descriptivealt; for purely decorative images, usealt=""so screen readers skip them entirely instead of reading a filename. - Always pair form controls with a
<label>using matchingfor/idattributes; never rely on aplaceholderalone, since it disappears once the user starts typing. - Keep heading levels sequential (don’t skip from
<h2>to<h4>) so the outline they form is accurate. - Use landmark elements (
<header>,<main>,<nav>,<footer>) once per page for the major regions so users can navigate by structure, not just by scrolling. - Set
langon the<html>element so pronunciation and hyphenation rules match the actual language of your content. - Test with a keyboard alone: if you cannot reach and activate every interactive element using only Tab, Shift+Tab, Enter, and Space, keyboard users can’t either.
- Remember that color and visual styling live in CSS, not HTML — but the underlying markup must still expose text alternatives, since color alone can never convey information to a screen reader.
Practice Exercises
Exercise 1
Write a small form with a single text input for a “Phone number” field. Use a properly associated <label>, and add an aria-describedby hint explaining the expected format (e.g. “Include area code”).
Exercise 2
Take this flat markup and rewrite it using landmark elements: a page with a site title, three navigation links, one paragraph of body content, and a copyright line. Decide which parts belong in <header>, <nav>, <main>, and <footer>.
Exercise 3
Find (or imagine) a page with an icon-only “search” button that has no visible text. Add the correct attribute so a screen reader announces it as “Search, button” instead of just “button.” Hint: think about which attribute supplies an accessible name when there’s no text content.
Summary
- Browsers build an accessibility tree from your DOM, giving every element a role, name, and state that assistive technology reads directly.
- Semantic elements like
<button>,<nav>, and headings come with built-in accessible roles — prefer them over generic<div>/<span>plus custom ARIA. alttext and<label>associations are the two most common and most impactful fixes for real-world accessibility gaps.- ARIA attributes (
aria-label,aria-labelledby,aria-describedby,aria-live,aria-expanded) fill in gaps only when native HTML semantics aren’t enough. - Sequential heading levels and landmark regions give non-visual users a navigable outline of the page.
- Keyboard operability is a fast, reliable manual test: if you can’t Tab to and activate something, neither can many real users.
