In Module 3 we closed our tour of the four POUR principles seen as requirements: we learned what WCAG demands in each criterion. Now the implementation part begins: how you meet those demands with real HTML and CSS. And we start where you should start, because it is the most direct, cheapest and most robust path to accessibility: semantic HTML. When you pick the right element, the browser hands you name, role and value (the three pillars of criterion 4.1.2 that we saw as a requirement) without your writing a single line of JavaScript or ARIA. A <button> already "is" a button for Marta's screen reader; a <nav> already "is" a navigation region Lucía can jump to. Semantics is, quite literally, accessibility for free.

In this lesson we build the skeleton of the Cursalia landing page and catalog using the right elements, and we will see, by contrasting good markup against bad, why the tag you choose completely changes the experience for assistive technologies.

Contents

  1. Why semantics gives you name/role/value for free
  2. Landmarks: the regions of the page
  3. Heading hierarchy h1–h6
  4. Lists: ul, ol and dl
  5. Accessible data tables
  6. Button vs link: the <div onclick> mistake
  7. Native form controls (a mention)
  8. Page attributes: lang, title and the skip link
  9. The Cursalia landing page: good vs bad structure
  10. Common mistakes and tips
  11. Exercises
  12. Conclusion

Why semantics gives you name/role/value for free

Recall the accessibility tree from Module 2: from your HTML, the browser builds a parallel version of the DOM that exposes to assistive technologies the role (what the element is), the name (what it is called) and the value/state (what situation it is in) of each component. Semantic HTML is how you "fill in" that tree with no effort.

If you write... The accessibility tree exposes... Extra cost
<button>Enroll</button> role=button, name="Enroll", focusable, activatable with Enter/Space Zero
<nav> role=navigation (jumpable landmark) Zero
<h2>Featured courses</h2> role=heading, level=2 (appears in the heading list) Zero
<div class="button" onclick> role=generic (nothing!), not focusable, not keyboard-activatable A lot: you have to rebuild everything with ARIA + JS

The practical takeaway is the golden rule of accessibility, which we will repeat in 04-03:

Use the native HTML element that already has the semantics you need before recreating them with ARIA and JavaScript.

Landmarks: the regions of the page

Landmarks are high-level regions that let a screen reader user like Marta jump straight to "the main content" or "the navigation" without listening to the whole page. NVDA offers a key to list landmarks just as it offers one to list headings.

HTML sectioning elements are automatically mapped to landmark roles:

HTML element Accessibility role (landmark) Use
<header> (page level) banner Site header: logo, search
<nav> navigation Navigation menus
<main> main Main content (one per page)
<aside> complementary Related content: filters, promos
<footer> (page level) contentinfo Footer: legal notices, contact
<section> with a name region Thematic section with a heading
<article> article Self-contained content (a course card)

A correct example of the frame of a Cursalia page:

<body>
  <header>
    <a href="/"><img src="logo-cursalia.svg" alt="Cursalia"></a>
    <nav aria-label="Main">
      <ul>
        <li><a href="/courses">Catalog</a></li>
        <li><a href="/pricing">Pricing</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>Learn at your own pace with Cursalia</h1>
    <!-- main content -->
  </main>

  <aside aria-label="Catalog filters">
    <!-- filters -->
  </aside>

  <footer>
    <p>© 2026 Cursalia</p>
  </footer>
</body>

Compared with the same frame built only from <div>s, which contributes no region at all:

<!-- INCORRECT: to the screen reader this is a soup of "generic groups" -->
<div class="header">...</div>
<div class="nav">...</div>
<div class="content">...</div>
<div class="footer">...</div>

For Marta's reader, the first version has five named regions to navigate; the second has none. Note the detail of aria-label="Main" on the <nav>: if there is more than one landmark of the same type (for example, the main navigation and another in the footer), you must distinguish them with a name. This is reinforced semantics, not replaced; we will cover it properly in 04-03.

graph TD
    A[body] --> B["header → banner"]
    A --> C["nav → navigation"]
    A --> D["main → main"]
    A --> E["aside → complementary"]
    A --> F["footer → contentinfo"]
    D --> G["article → article (course card)"]

Heading hierarchy h1–h6

Headings are not "big text": they are the navigable outline of the page. Marta presses the H key in NVDA to jump from heading to heading and build a mental map of the content, and she can press 2 to reach only the level-2 ones. That is why the hierarchy must reflect the logical structure, not the visual size (size is CSS's job).

Criterion 1.3.1 rules applied to headings:

  • A single <h1> per page, describing the overall topic.
  • Do not skip levels going down: after an <h2> you don't jump straight to an <h4>.
  • The level indicates hierarchy, not appearance. If you want a small h2, shrink it with CSS.
<!-- CORRECT: hierarchy without skips -->
<main>
  <h1>Course catalog</h1>
    <h2>Featured courses</h2>
      <h3>Web Accessibility</h3>
      <h3>Modern JavaScript</h3>
    <h2>What's new</h2>
      <h3>Systems design</h3>
</main>
<!-- INCORRECT: the heading is chosen by its size, not by hierarchy -->
<h1>Course catalog</h1>
<h4>Featured courses</h4>   <!-- jumps from 1 to 4 -->
<h2>Web Accessibility</h2>   <!-- and now up to 2: the outline is incoherent -->

Lists: ul, ol and dl

When you mark a set of items as a list, the screen reader announces "list of 8 items" and lets you navigate through it. That context is lost if you use <div> or <br>. Three types:

  • <ul>: unordered list (order doesn't matter). The navigation menu, the catalog cards.
  • <ol>: ordered list (sequence matters). The enrollment steps.
  • <dl>: description list (term/description pairs). A course fact sheet: "Duration: 20 h", "Level: Intermediate".
<!-- CORRECT: the course grid IS a list -->
<ul class="course-grid">
  <li><article>...Web Accessibility card...</article></li>
  <li><article>...Modern JavaScript card...</article></li>
</ul>

<!-- CORRECT: course metadata as a description list -->
<dl>
  <dt>Duration</dt><dd>20 hours</dd>
  <dt>Level</dt><dd>Intermediate</dd>
  <dt>Language</dt><dd>English</dd>
</dl>
<!-- INCORRECT: it looks like a list, but it isn't one for anyone but the eye -->
<div class="course-grid">
  <div>...card...</div>
  <div>...card...</div>
</div>

Accessible data tables

A data table (not a layout table, which you should not use) needs semantic markup so Marta understands which row and column each cell belongs to. The key pieces:

  • <caption>: the table's title, the first thing the reader announces.
  • <th> for header cells, versus <td> for data.
  • scope="col" / scope="row": indicates whether a header governs a column or a row.
  • <thead> / <tbody>: separate headers from the body.
<!-- CORRECT: comparison of Cursalia plans -->
<table>
  <caption>Subscription plan comparison</caption>
  <thead>
    <tr>
      <th scope="col">Plan</th>
      <th scope="col">Price/month</th>
      <th scope="col">Courses included</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Basic</th>
      <td>€9</td>
      <td>50</td>
    </tr>
    <tr>
      <th scope="row">Pro</th>
      <td>€19</td>
      <td>All</td>
    </tr>
  </tbody>
</table>

Without scope or <th>, the reader recites a string of numbers without saying "Pro, Price/month, €19": the relationship between data and header is lost.

Button vs link: the <div onclick> mistake

This is one of the most wrong-headed decisions in practice. The rule is simple:

  • <a href> (link): navigates somewhere else (another page, another section, an anchor). Role link. The reader announces it as "link".
  • <button>: performs an action on the current page (open the filter, submit the form, play the video). Role button. The reader announces it as "button".

Both are focusable and keyboard-operable out of the box: the link is activated with Enter, the button with Enter and Space. A <div> or <span> with onclick has none of those properties.

<!-- CORRECT -->
<a href="/courses/web-accessibility">See the Web Accessibility course</a>
<button type="button">Open filters</button>
<!-- INCORRECT: the classic trap -->
<div class="btn" onclick="openFilters()">Open filters</div>

That <div> fails in a cascade for Lucía (keyboard only) and Marta:

  1. It doesn't receive focus: Lucía can't tab to it (you'd have to force tabindex="0").
  2. It isn't keyboard-activatable: even if it had focus, Enter/Space do nothing (you'd have to listen for keys with JS).
  3. It has no role: Marta doesn't hear "button", she hears loose text, and doesn't know it's clickable.

To "fix" that <div> you have to add role="button", tabindex="0", keyboard handlers for Enter and Space... that is, reimplement by hand everything <button> gives you for free. Recreating native controls with ARIA is exactly what we will see in Module 5; here the lesson is to avoid having to do it.

Native form controls (a mention)

The elements <input>, <select>, <textarea> and <button> come with role, focus, keyboard operation and states built in. An <input type="checkbox"> already communicates checked/unchecked; a <select> is already an operable dropdown list. Leveraging them is the foundation of an accessible form, but their association with labels, grouping and errors are the full topic of lesson 04-02, so here we just note that they exist and that they are the first choice.

Page attributes: lang, title and the skip link

Three document details with a big impact:

  • lang on <html>: tells the reader which language to pronounce. Without it, NVDA might read Cursalia's English with the wrong phonetics and become unintelligible. If there is a fragment in another language inside, you mark it with lang on that element (this connects with the i18n of Module 7).
  • The page <title>: it is the first thing the reader announces on load and what you see in the tab. It must be unique and descriptive: Course catalog · Cursalia.
  • Skip link ("skip to content"): a link that is the page's first focus and goes straight to <main>, so Lucía doesn't have to tab through the whole header and menu on every page.
<!doctype html>
<html lang="en">
<head>
  <title>Course catalog · Cursalia</title>
</head>
<body>
  <!-- First focusable element on the page -->
  <a class="skip-link" href="#content">Skip to main content</a>
  <header>...</header>
  <main id="content" tabindex="-1">
    <h1>Course catalog</h1>
  </main>
</body>
</html>

The skip link is usually hidden visually and shown only when it receives focus, so as not to bother the mouse user while remaining available to the keyboard:

.skip-link {
  position: absolute;
  left: -9999px;      /* off-screen, but present in the DOM */
}
.skip-link:focus {
  left: 1rem;
  top: 1rem;          /* appears when you tab to it */
}

The Cursalia landing page: good vs bad structure

Let's put it all together by comparing two versions of the same landing page.

<!-- INCORRECT: all divs, no real headings, "buttons" that are divs -->
<div class="top">
  <div class="menu"><span onclick="go('/courses')">Catalog</span></div>
</div>
<div class="hero">
  <div class="big">Learn at your own pace</div>
  <div class="btn" onclick="register()">Start free</div>
</div>
<div class="cards">
  <div class="card">Web Accessibility</div>
  <div class="card">Modern JavaScript</div>
</div>

For Marta this is a page with no regions, no headings and no controls: impossible to traverse efficiently. For Lucía, nothing receives focus.

<!-- CORRECT: same visual design, complete semantics -->
<a class="skip-link" href="#content">Skip to content</a>
<header>
  <nav aria-label="Main">
    <ul><li><a href="/courses">Catalog</a></li></ul>
  </nav>
</header>
<main id="content">
  <h1>Learn at your own pace with Cursalia</h1>
  <p><a class="cta" href="/register">Start free</a></p>

  <section aria-labelledby="featured">
    <h2 id="featured">Featured courses</h2>
    <ul class="cards">
      <li>
        <article>
          <h3><a href="/courses/web-accessibility">Web Accessibility</a></h3>
          <img src="acc.webp" alt="">   <!-- alt in 05-04 -->
        </article>
      </li>
      <li>
        <article>
          <h3><a href="/courses/javascript">Modern JavaScript</a></h3>
        </article>
      </li>
    </ul>
  </section>
</main>

CSS can make both look identical. The difference isn't in the pixels, but in the structural information one exposes and the other destroys. (The image's alt="" makes sense and we'll study it in depth in 05-04.)

Common Mistakes and Tips

  • Using <div>/<span> for everything. This is the root mistake. Before you write a <div>, ask yourself whether an element exists that means what you want to say.
  • Choosing a heading by its size. An <h3> is not "a medium-sized title"; it is a hierarchical level. Adjust the size with CSS, never by changing the level.
  • Multiple <h1>s or level skips. They break the page outline. One <h1>, and go down one level at a time.
  • A button that navigates or a link that acts. If it changes the URL, it's <a>; if it acts on the page, it's <button>. Don't swap them for aesthetics.
  • Tables for layout. Reserve <table> for tabular data; use CSS Grid/Flexbox for visual layout.
  • Forgetting lang and <title>. They are one line each and drastically improve reading aloud.
  • Tip: navigate your own page using only the keyboard (Tab, Enter) and a reader's heading list. If you can't traverse it, your semantics are incomplete.

Exercises

Exercise 1: turn div soup into landmarks

You have this fragment of the Cursalia header. Rewrite it with the correct semantic elements, adding the necessary name to the nav.

<div class="header">
  <div class="logo">Cursalia</div>
  <div class="menu">
    <div class="item"><span onclick="go('/courses')">Catalog</span></div>
    <div class="item"><span onclick="go('/pricing')">Pricing</span></div>
  </div>
</div>

Exercise 2: fix the heading hierarchy

A designer chose the headings by their visual size. Correct the levels so the hierarchy is logical and skip-free.

<h1>Course fact sheet: Web Accessibility</h1>
<h3>Description</h3>
<h2>Syllabus</h2>
<h4>Module 4: HTML and CSS</h4>

Exercise 3: button or link

For each control, decide whether it should be <a href> or <button> and write it:

  1. "Go to the pricing page".
  2. "Play the video lesson" (on the same page).
  3. "Download the certificate" (navigates to a PDF).
  4. "Open the catalog filters dropdown".

Solutions

Solution 1. Each structural <div> is replaced by its semantic equivalent; the clickable "items" that navigate are real links:

<header>
  <a href="/" class="logo">Cursalia</a>
  <nav aria-label="Main">
    <ul>
      <li><a href="/courses">Catalog</a></li>
      <li><a href="/pricing">Pricing</a></li>
    </ul>
  </nav>
</header>

The aria-label="Main" is necessary because there is probably another <nav> (for example in the footer) and they must be distinguished.

Solution 2. It must go down progressively without skips and with a single <h1>:

<h1>Course fact sheet: Web Accessibility</h1>
<h2>Description</h2>
<h2>Syllabus</h2>
<h3>Module 4: HTML and CSS</h3>

"Description" and "Syllabus" are sibling top-level sections (h2); the module hangs off the syllabus (h3).

Solution 3.

  1. Link: navigates to another page → <a href="/pricing">Go to pricing</a>.
  2. Button: triggers something on the same page → <button type="button">Play</button>.
  3. Link: goes to a resource (the PDF) → <a href="/certificate.pdf" download>Download certificate</a>.
  4. Button: opens/closes a panel on the page → <button type="button" aria-expanded="false">Filters</button>. We'll see aria-expanded and its behavior in 04-03 and 05-01.

Conclusion

Semantic HTML is the foundation of everything else in accessibility: by choosing elements well you get name, role and value for free, navigable regions, a heading outline and keyboard-operable controls, without writing any extra code. We have laid out the Cursalia landing page with landmarks, heading hierarchy, lists, data tables, and correct links and buttons, and we have set the golden rule: use the native element before recreating it with ARIA (an idea we'll expand in 04-03).

The natural next step is where semantics becomes trickiest and where the 3.3.x criteria concentrate: forms. In lesson 04-02 · Accessible Forms we'll apply all of this to Cursalia's enrollment form —labels, groupings, required fields, hints and error messages— so that Marta, Hugo and Lucía can complete it without barriers.

© Copyright 2026. All rights reserved