Skip to content
Chapter 17Lesson 3

Landmarks and the heading outline

Structure your JSX with semantic HTML landmark elements and a clean heading outline so every page is navigable by screen readers, crawlers, and browser tooling.

The last lesson handed you <body>. The root layout owns <html lang> and hands <head> to the metadata API, and everything you build from here lives inside that one <body>. That leaves the question every page forces on you: what structure goes inside it?

The Acme dashboard, your invoicing app “Acme — Invoicing for small teams”, is a top bar with the logo and primary navigation, a main content area, and a footer. You have to mark that up. The naive answer is a pile of <div>s: one for the top bar, one for the nav inside it, one for the content, one for the footer. It renders, it looks right, so you ship it.

Here is that same page written two ways.

<div className="page">
<div className="top-bar">
<div className="logo">Acme</div>
<div className="nav">Dashboard · Invoices · Customers</div>
</div>
<div className="content">
<div className="title">Invoices</div>
</div>
<div className="footer">© 2026 Acme, Inc.</div>
</div>

Four <div>s and nothing else. The browser draws exactly what you’d expect, and nothing here is wrong to a sighted user with a mouse. But every container is a generic box that means nothing.

Open both in a browser and you cannot tell them apart: the pixels are identical. That is the reason this lesson exists. The difference between these two pages is invisible to you, but it matters to everyone else who reads the page. A screen reader reads the second one as a navigable map and the first one as one undifferentiated wall. A search crawler finds the real content in the second and has to guess at it in the first. Reader mode, “skip to content,” and the browser’s own outline tools all work off the second version and do nothing with the first.

This is the same frame the last lesson left you with: the document shell is a set of decisions read by machines you never see. Landmarks and headings are the navigable layer of that machine-read document, the part that says not just “here is a page” but “here is how to move around it.” By the end of this lesson you’ll structure the Acme shell so its outline is legible to those machines, using two cooperating systems: landmarks (the region map) and headings (the content map). You’ll also learn how to verify your work without ever opening a screen reader. The floor for this project, as for every real product, is WCAG 2.2 level AA, and these two systems are most of how you clear it.

A screen reader reads your page as a map, not a wall of text

Section titled “A screen reader reads your page as a map, not a wall of text”

Before any list of elements, you need a clear picture of who the second audience is and how they move, because every rule in this lesson has a “for whom” attached to it. Once you can picture that reader, the rules stop looking like pedantry and start looking obvious.

You, a sighted user with a mouse, scan a page in two dimensions at once. You take in the whole layout in a glance: the bar across the top is navigation, the big block in the middle is the content, the small grey row at the bottom is the footer. Then you jump your eyes wherever you want. Size, position, and weight tell you what everything is before you read a word.

A screen-reader user gets none of that. The page reaches them one element at a time, as a linear stream. If the only way to move through it were top to bottom, every page would mean wading through the same logo, the same nav, and the same cookie banner before reaching the part they came for. So they don’t move top to bottom. They jump. Screen readers expose the page as a set of shortcuts: jump to the next landmark, jump to the next heading, jump to the next link, or pull up a list of all the headings and pick one. In NVDA and JAWS the D key cycles through landmarks and H through headings; VoiceOver has the same moves in its rotor. To them the page isn’t a wall of text. It’s a map they navigate by region and by heading, the same way your eyes navigate it by position and size.

That map is not your HTML directly. The browser computes a second tree from your DOM, the accessibility tree , and that is what assistive tech reads. Each node in it carries a role (“this is a button,” “this is the main region”), a name (“Save,” “Primary navigation”), and a state (“pressed,” “disabled”). Your semantic HTML is the input to that tree. A <header> becomes a banner landmark, and a <nav> becomes a navigation landmark. A <div> becomes nothing navigable at all: it’s a styling box with no role, invisible to every jump command.

You can see this tree yourself. In Chrome, open DevTools, select an element in the Elements panel, and look at the Accessibility pane, which shows the role, name, and state the browser computed for that node. There’s much more to the accessibility tree, and a later chapter on the full accessibility baseline goes deep. Here it’s enough to know it exists and where to find it.

This is the picture to hold onto. On the left is the DOM you wrote. On the right is the accessibility tree the screen reader actually travels through.

The DOM you wrote
<div className="page">
<header> Acme
<nav> Dashboard · Invoices
<main> Invoices
<footer> © 2026 Acme, Inc.
The accessibility tree
banner jump target
navigation jump target
main jump target
contentinfo jump target
<div> no landmark — dropped

Your semantic elements (left) become the landmarks a screen reader jumps between (right), matched here by colour. A plain <div> produces no landmark, so it’s invisible to every jump command.

Read it left to right and the mental model follows: your markup is the API the accessibility tree is built from. Semantic elements feed that tree, and <div>s give it nothing. Every choice in the rest of this lesson is a choice about what shows up on the right-hand side of that picture.

Landmarks are the coarse map: the regions a screen-reader user jumps between. Seven elements produce them, and the fastest way to learn them is not as a flat list but in the three jobs they do: framing the page, marking navigation, and grouping content. You’ll see this set called “six landmarks” almost as often as “seven,” because two of the seven only become landmarks under a condition we’ll get to. Once you’ve learned all seven, that discrepancy will make sense.

Three elements frame every page. They’re the first thing a screen-reader user reaches for, because they answer “top, middle, or bottom?”

<header> is introductory content, the band across the top of the page. Your logo and primary navigation live here. There’s one page-level <header>, and in the accessibility tree it becomes the banner landmark. One nuance is worth naming now so you don’t over-apply the rule: a <header> is also allowed inside an <article> or <section> to introduce that piece of content, such as a blog post’s title and byline. “One header” means one page-level header, not one header total.

<main> is the unique main content of the page, the part that’s actually about this page rather than the chrome repeated on every page. The rule here is strict: exactly one <main> per page, and it’s never nested inside a header, nav, aside, or footer. This is the element that powers “skip to main content,” and it’s the signal a search crawler uses to find where the real content begins. To decide what belongs inside it, ask whether something would still be here if you navigated to a different page. If yes, it’s chrome, and it lives outside <main>.

<footer> is closing content: copyright, the footer sitemap, contact info. One page-level <footer> becomes the contentinfo landmark. As with <header>, nested footers are fine: an <article> can carry its own <footer> with the author and publish date.

<nav> marks a region of navigation links, and it becomes the navigation landmark. The word that matters is navigation: <nav> is not for every list of links. A handful of related links at the bottom of an article is just a list, so it doesn’t earn a landmark. Reserve <nav> for the major navigation blocks: the primary nav in the header, the section links down the sidebar, the sitemap in the footer.

This has a consequence. A real app has more than one of these, at least a primary nav up top and a footer sitemap. The moment you have two <nav>s, a screen-reader user pulling up the landmark list hears “navigation, navigation” with no way to tell which is which. That’s a real problem, and it’s exactly the one the next section solves.

The grouping elements: section and article

Section titled “The grouping elements: section and article”

These are the two that trip people up, and the two that only sometimes count as landmarks.

<article> is for self-contained, independently distributable content. The test is simple and worth memorizing: would this still make sense if you pasted it somewhere else entirely? A blog post, a single comment in a thread, a forum reply, a product card in a catalog: each stands on its own, so each is an <article>. A dashboard metric card showing “Revenue this month: $48,200” is not an article. It makes no sense pasted into another page, so reaching for <article> here is a common misuse. It’s data, not a self-contained document.

<section> is a generic thematic grouping: a chunk of related content that forms one part of the page. The test for it is short: it’s a section if it has, or should have, a heading. “Overdue invoices” with its own <h2> is a section. A grouping of content with no heading and no name is just a <div>. That test is also the bridge to the next system, because it ties a section to a heading, and that link between landmarks and headings is one we’ll lean on shortly.

Here’s the catch that explains the “six versus seven” confusion: <section> and <article> only become navigable landmarks when they have an accessible name, either a heading they point to or a literal label. Without a name they’re still meaningful grouping in the DOM, but they’re not jumpable regions in the accessibility tree. We’ll wire up that name in the next section. For now, just know that a bare <section> is structure, and a named <section> is a landmark.

<aside> is for content tangentially related to what’s around it: a “related articles” rail next to a blog post, a tip callout beside a form, contextual help. It becomes the complementary landmark.

There’s a trap here, and it’s the most common landmark mistake there is: the app’s left sidebar is usually not an <aside>. If that sidebar holds your section links, such as Dashboard, Invoices, and Customers, it’s navigation, so it’s a <nav>. <aside> is for content that sits beside the main content and complements it, not for the navigation chrome of your app. The word “sidebar” describes a position on screen; it tells you nothing about the element. Decide by what the thing is, not where it sits.

That covers the seven. Here’s how the page-frame trio plus a nav look as the skeleton of the Acme shell, building straight on the intro’s “Landmarks” tab:

<body>
<header>
<a href="/">Acme</a>
<nav>{/* primary navigation */}</nav>
</header>
<main>
<h1>Invoices</h1>
{/* sections go here */}
</main>
<footer>
<nav>{/* footer sitemap */}</nav>
</footer>
</body>

We’ll grow this exact skeleton across the rest of the lesson, adding names, headings, and content, until it’s a complete, correct shell. For now, picture how those regions sit on the actual page.

<header>
<a> Acme
<nav> Dashboard · Invoices · Customers
<main>
Invoices <h1>
<section>
Overdue <h2>
<section>
This month <h2>
<footer>
<nav> Product · Pricing · Docs · Contact
© 2026 Acme, Inc.

The Acme shell as landmark regions. Every box is a decision about what that region is, and a <div> here would erase it from the map.

The pattern to hold onto is this: every UI you build maps onto a small set of regions, and your job is to name each one with the right element. Try it on real fragments of the Acme dashboard.

Each fragment is part of the Acme dashboard. Sort it into the landmark element it should use. Drag each item into the bucket it belongs to, then press Check.

header Page-level introductory band
nav A major navigation block
main The page's unique content
aside Content beside the main content
article Self-contained, distributable
footer Page-level closing content
The site logo and the top row of links
The list of dashboard section links down the left side
The sitemap links at the bottom of every page
The invoice list — the page’s primary content
A single customer testimonial that could be quoted on the marketing site
A “Pro tip” callout sitting beside the invoice form
The copyright and company address row
One comment in an invoice’s activity thread

Multiple landmarks of the same type need names

Section titled “Multiple landmarks of the same type need names”

That problem from the last section is now in front of you. The Acme shell has two <nav>s, the primary one in the header and the sitemap in the footer, and once we add <section>s for “Overdue” and “This month,” it’ll have several of those too. A screen-reader user pulls up the landmark list and hears “navigation, navigation,” or, with the sections, “region, region, region.” Which navigation is the primary one? Which region is billing? There’s no way to tell. A landmark with no name is a door with no sign on it.

The fix is to give each repeated landmark a distinguishing name, and two attributes do it.

aria-label puts the name in literal text. You write the words, and they become the landmark’s accessible name. Use this when there’s no visible text on the page that already names the region:

<nav aria-label="Primary">{/* ... */}</nav>
<nav aria-label="Footer">{/* ... */}</nav>

Now the landmark list reads “Primary navigation, Footer navigation”: two distinct doors with signs.

aria-labelledby names the landmark by reference to another element’s text, using that element’s id. Use this when a visible heading already says the name out loud, so you point the region at its own heading instead of retyping the words:

<section aria-labelledby="overdue-heading">
<h2 id="overdue-heading">Overdue</h2>
{/* ... */}
</section>

The <section>’s accessible name becomes “Overdue,” borrowed straight from the <h2>. There’s no duplicated text, and if you ever rename the heading, the landmark’s name follows automatically, because there’s only one source of truth.

That comparison gives you the decision rule, and you’ll apply it often: if a visible heading already names the region, use aria-labelledby to point at it; otherwise use aria-label with literal text. Every repeated same-type landmark gets a name that sets it apart from its siblings: two navs named “Primary” and “Footer,” not two navs both named “Navigation.”

One note on the JSX surface, reinforcing the last two lessons: aria-label and aria-labelledby stay kebab-case in JSX, since they’re among the handful of attributes that don’t get camelCased, and their values are plain strings. The id that aria-labelledby points at is an ordinary HTML id, the same one you’d use anywhere else.

These two attributes are your first taste of ARIA , and there’s a principle the field repeats that’s worth seeding now: no ARIA is better than bad ARIA. ARIA is for filling gaps that native HTML leaves open, and naming a landmark that would otherwise be ambiguous is exactly that kind of legitimate gap. You’re not overriding what the element is; you’re adding the one thing it’s missing. A later lesson in this chapter, and a later chapter on accessibility, cover ARIA in full; here you need only these two naming attributes.

Here’s the Acme shell again, now with its landmarks named, one step at a time.

<body>
<header>
<a href="/">Acme</a>
<nav aria-label="Primary">{/* primary navigation */}</nav>
</header>
<main>
<h1>Invoices</h1>
<section aria-labelledby="overdue-heading">
<h2 id="overdue-heading">Overdue</h2>
{/* overdue invoices */}
</section>
<section aria-labelledby="month-heading">
<h2 id="month-heading">This month</h2>
{/* this month's invoices */}
</section>
</main>
<footer>
<nav aria-label="Footer">{/* sitemap */}</nav>
</footer>
</body>

The header’s nav gets a literal name. In the landmark list it now reads “Primary navigation,” not just “navigation.”

<body>
<header>
<a href="/">Acme</a>
<nav aria-label="Primary">{/* primary navigation */}</nav>
</header>
<main>
<h1>Invoices</h1>
<section aria-labelledby="overdue-heading">
<h2 id="overdue-heading">Overdue</h2>
{/* overdue invoices */}
</section>
<section aria-labelledby="month-heading">
<h2 id="month-heading">This month</h2>
{/* this month's invoices */}
</section>
</main>
<footer>
<nav aria-label="Footer">{/* sitemap */}</nav>
</footer>
</body>

This section is named by reference. aria-labelledby points at the heading’s id, so the region’s name is “Overdue,” borrowed from the visible heading and never retyped.

<body>
<header>
<a href="/">Acme</a>
<nav aria-label="Primary">{/* primary navigation */}</nav>
</header>
<main>
<h1>Invoices</h1>
<section aria-labelledby="overdue-heading">
<h2 id="overdue-heading">Overdue</h2>
{/* overdue invoices */}
</section>
<section aria-labelledby="month-heading">
<h2 id="month-heading">This month</h2>
{/* this month's invoices */}
</section>
</main>
<footer>
<nav aria-label="Footer">{/* sitemap */}</nav>
</footer>
</body>

The same pattern for “This month.” Two sections, two distinct names, no more “region, region.”

<body>
<header>
<a href="/">Acme</a>
<nav aria-label="Primary">{/* primary navigation */}</nav>
</header>
<main>
<h1>Invoices</h1>
<section aria-labelledby="overdue-heading">
<h2 id="overdue-heading">Overdue</h2>
{/* overdue invoices */}
</section>
<section aria-labelledby="month-heading">
<h2 id="month-heading">This month</h2>
{/* this month's invoices */}
</section>
</main>
<footer>
<nav aria-label="Footer">{/* sitemap */}</nav>
</footer>
</body>

The footer’s nav is named too. Now the two navigation landmarks are “Primary” and “Footer,” so a screen-reader user can jump straight to the one they want.

1 / 1

Landmarks are one map. Headings are a second, separate map, and confusing the two is a common mistake, so it’s worth drawing the line between them clearly.

Landmarks answer “what regions does this page have?” Headings answer “what is the content structure within and across those regions?” A screen-reader user navigates headings as a table of contents, jumping heading to heading, completely independently of the landmark jumps. The two systems run in parallel. A <section> is not a heading, and neither is a <main>. The heading is a separate element, an <h1> through <h6>, that lives inside those regions and describes their content. Keep that separation clear, because everything else in this section depends on it.

The heading elements run <h1> through <h6>, and they obey three hard rules. These are testable facts, not style preferences:

Exactly one <h1> per page. It’s the page’s primary heading; for the Acme invoices page, that’s <h1>Invoices</h1>. One page, one <h1>.

Levels descend by significance, and you never skip one. <h2> is a major subsection, <h3> is a subsection of an <h2>, and so on down. Going from <h1> straight to <h3> with no <h2> between them is a real accessibility violation, because it breaks the outline tree. A screen-reader user hears the gap and reads it as “wait, did I miss a heading? Is there content I skipped?” You fix a skip by either inserting the missing level or downgrading the deeper one. You may have heard that some old HTML feature auto-calculates heading levels per <section>; it doesn’t, and it never reliably did. The outline is exactly the <h1><h6> levels you write, nothing cleverer.

Level is determined by outline position, not visual size. This is the rule worth slowing down on. The number in <h2> is a structural claim, “this is a second-level subsection,” not a claim about size. It does not mean “the medium-big text.” If your <h2> needs to look small, or your <h3> needs to look large, that’s a styling decision you make separately. You never pick a heading level to get a font size.

That rule exists to prevent a bug you will see constantly in real codebases, and it’s the one place a styling utility earns a mention. Compare these two:

<div className="text-2xl font-bold">Billing</div>

Renders as big bold text, contributes nothing. To your eyes it’s a heading; to the accessibility tree it’s a <div>, with no role at all. It adds nothing to the outline, so a screen-reader user jumping by heading sails right past “Billing” as if it weren’t there. There’s a visual hierarchy, but no navigable one.

That’s the whole discipline in two snippets: the look and the level are separate knobs. Changing the level is a structure decision, and changing the class is a style decision. Tailwind, where text-2xl and the rest come from, is the next chapter’s entire subject; here it makes a single cameo to drive that one point home.

Stacked up, the heading levels form a tree, a literal outline of the page. Here’s the Acme invoices page as that tree, alongside what a broken one looks like.

h1 Invoices
h2 Overdue
h2 This month
h3 Drafts
h3 Sent
A valid outline: one h1, and levels descend one step at a time. This is the table of contents a screen-reader user navigates.

You don’t need a screen reader to catch these. Browser tooling surfaces the outline directly: Chrome’s accessibility tooling and a Lighthouse audit both report heading order, and the WAVE and axe extensions flag a skipped level on the spot. Recognition is the goal here. You’ll meet the full audit workflow later; for now, know these tools exist and what they’re checking.

So the two maps cooperate, and you can now see exactly where they touch. A <section> is a landmark region, and its <h2> is the node that region contributes to the heading outline. aria-labelledby, the attribute from the last section, is the wire connecting the two systems: it makes the landmark borrow its name from the heading. Neither map replaces the other. A page with great landmarks and a broken heading outline is only half-navigable, and so is the reverse, so a finished page needs both.

Try keeping an outline valid yourself. The markup below has the level blanked on each heading’s opening tag. Pick the level so the outline is correct: one <h1>, and no skipped levels on the way down. Each closing tag matches whichever level you pick.

Pick the level for each heading so the page has exactly one h1 and no skipped levels. Pick the right option from each dropdown, then press Check.

<main>
<h___>Invoices</h…>
<section aria-labelledby="overdue">
<h___ id="overdue">Overdue</h…>
</section>
<section aria-labelledby="month">
<h___ id="month">This month</h…>
<h___>Sent this week</h…>
</section>
</main>

The content elements that fill the landmarks

Section titled “The content elements that fill the landmarks”

Landmarks are the regions and headings are the outline, but a region with only a heading is empty. Something has to fill it: the actual prose, the actual content. That’s a small, well-worn set of elements, and the only decision that matters is to reach for the element that carries meaning and fall back to the meaningless ones only when nothing fits.

<p> is the default block of body text. A paragraph of prose goes in a <p>, not in a bare <div> and not as raw text dumped straight into <main>. It’s the most basic semantic container there is, and it’s easy to skip precisely because it’s so basic, but text outside a <p> is text with no structure around it.

<div> and <span> are the fallbacks, the elements with no semantic meaning. <div> is block-level, <span> is inline, and that’s the entire difference. The rule for both is the same: reach for them only when no semantic element fits, when all you need is a box to hang styles or grouping on with no meaning to convey. A wrapper that exists purely to apply a layout is a legitimate <div>. A <div> standing where a <header>, <nav>, <main>, <section>, or heading belongs is not. That’s the div soup from the start of this lesson, and now you can name precisely what’s wrong with it. Every level of nesting is a decision, and a <div> where a landmark or a heading belongs is a silent regression: nothing breaks, nothing looks wrong, and a whole layer of the page quietly vanishes from the accessibility tree. This closes the loop the intro opened.

<br /> and <hr /> round out the set, and both are narrow and semantic. <br /> is a meaningful hard line break, the kind inside a postal address or a line of poetry, where the line break is part of the content’s meaning. It is never for visual spacing between blocks; that’s a margin, which the next chapter’s styling tools own. <hr /> is a thematic break in content, a real shift in topic, not a decorative divider line, which is a border utility’s job. Both are void elements, so they self-close in JSX, the rule from the first lesson of this chapter. You’ll reach for them rarely, and recognizing when they’re the wrong tool matters more than using them.

One more element family belongs in these regions, and it’s big enough to get its own treatment next: lists. Any sequence of related items, including the nav links inside your <nav>, belongs in a list (<ul>, <ol>, <li>), not a stack of <div>s. The next lesson, on actions, navigations, and item sequences, covers lists in full, so we’ll just name them here as the right home for related items and move on.

Put the content elements together with what you’ve built and a filled-in region looks like this: a heading, a paragraph, and a <div> doing honest work as a layout wrapper.

<section aria-labelledby="overdue-heading">
<h2 id="overdue-heading">Overdue</h2>
<p>You have 3 overdue invoices totaling $12,400.</p>
<div className="grid">{/* invoice cards */}</div>
</section>

The <section>, <h2>, and <p> all carry meaning. The <div> carries none, and that’s correct here, because all it does is hold a grid layout. That’s the <div> doing its actual job, not standing in for an element that should have had a name.

Putting the shell together and checking it

Section titled “Putting the shell together and checking it”

Each section of this lesson grew the same shell a little further. Here it is whole: the complete Acme shell, semantically structured, with both maps in place. This is the deliverable, the structure that goes inside the <body> the last lesson handed you.

A page inside app/
export default function InvoicesPage() {
return (
<>
<header>
<a href="/">Acme</a>
<nav aria-label="Primary">{/* Dashboard, Invoices, Customers */}</nav>
</header>
<main>
<h1>Invoices</h1>
<section aria-labelledby="overdue-heading">
<h2 id="overdue-heading">Overdue</h2>
<p>You have 3 overdue invoices totaling $12,400.</p>
</section>
<section aria-labelledby="month-heading">
<h2 id="month-heading">This month</h2>
<p>14 invoices sent, 9 paid.</p>
</section>
</main>
<footer>
<nav aria-label="Footer">{/* sitemap links */}</nav>
<p>© 2026 Acme, Inc.</p>
</footer>
</>
);
}

It’s under-styled on purpose, since there’s no Tailwind yet; that’s the next chapter. But it is semantically complete: one banner, two named navigation landmarks, one main, two named regions, one contentinfo, and a clean <h1> to <h2> outline. Everything in this shell slots inside the <body> from the root layout. Landmarks live in the page or a nested layout, never up next to the <html>/<body> line, which is the root layout’s exclusive territory. This example lives in a file inside app/; how page and layout files compose is a later chapter’s topic, so here just read it as “the content that fills <body>.”

Here’s the payoff of “make the invisible visible”: you can verify all of this without ever launching a screen reader. This is what an experienced engineer actually does before calling structure done:

  • Open DevTools → Elements → Accessibility. Click through your regions and confirm each shows the role you expected (banner, navigation, main, region, contentinfo) and that your named navs show the right accessible name. If a region you meant to be a landmark shows no role, you’ve got a <div> where an element should be.
  • Run Lighthouse, or the axe extension. Both have an accessibility audit that flags a skipped heading level, a missing <main>, and unnamed duplicate landmarks automatically. Green here means your two maps are well-formed.
  • Tab through the page. Confirm the focus order makes sense and matches the reading order. Full keyboard and focus work is a later chapter; this is a quick sanity check.

There’s one affordance these structured pages enable that’s worth knowing by name, even though we won’t build it here: the “skip to main content” link. Every page repeats the same header and nav, and a keyboard or screen-reader user shouldn’t have to tab through all of it on every single page just to reach the content. The fix is a link, a visually-hidden <a href="#main"> paired with id="main" on your <main>, placed as the very first focusable thing on the page and revealed when it receives focus. The first Tab then offers “skip to main content,” and one keypress jumps straight past the chrome. It’s the canonical first child of the body, and it only works because you have a <main> to point at. The visually-hidden technique and the focus mechanics belong to a later chapter on focus management; for now, recognize the pattern and know your <main> is what makes it possible.

So far you’ve recognized good structure; now write it yourself. Below is the div-soup shell from the start of the lesson. Refactor it into semantic landmarks with a valid heading outline.

Refactor this div-based shell into semantic landmarks with a valid heading outline: one <main>, one <h1>, named navigation, and no skipped heading levels.

Preview

    Step back and hold the whole model in one frame. A page is two cooperating maps. Landmarks are the region map: the handful of elements (<header>, <nav>, <main>, <aside>, <article>, <section>, <footer>) that carve the page into jumpable regions for the machines and assistive tech reading it. Headings are the content map: one <h1>, levels descending without skips, the level chosen by outline position and never by font size. Reach for the semantic element first, drop to <div>/<span> only when none fits, and name repeated landmarks with aria-label, or with aria-labelledby when a visible heading already says it. It’s the same content as the div soup you opened with, and the same pixels on the screen, but a page that is finally legible to the second audience, the one you’ll never see using it.

    These are bookmark-for-later references, not required reading: the canonical sources for the landmark elements and the heading outline.