← Back to Blog

Main and nav landmarks, one title, unique IDs: the skeleton screen readers rely on

· 12 min read Main and nav landmarks, one title, unique IDs: the skeleton screen readers rely on

Three rows in the Accessibility card on the Mega Analyzer's Perf + AI tab read <main> landmark, <nav> landmark and All IDs unique. A fourth, Document title set, sits on the A11y (WCAG) tab. Together they test whether the page has a skeleton a screen reader can move through: one region marked as the main content, a region marked as navigation, a title to announce when the page loads, and id attributes that each point at exactly one element. None of these rows reads what the page says. They read whether the page has a shape.

What the check actually tests

Everything here comes from the server-rendered HTML the analyzer fetched, parsed into a document and queried. Your JavaScript never runs.

<main> landmark passes when the document contains a <main> element or any element carrying role="main". The fail row is the same title with a red cross and no detail text, because there is nothing to itemize: the element is there or it is not. Two <main> elements still pass this row (the selector only asks whether at least one exists); the shared WCAG module catches that case, described below.

<nav> landmark passes on a <nav> element or any element with role="navigation". Same shape: pass or fail, no detail.

All IDs unique walks every element with an id attribute, counts each value, and passes when no value occurs more than once. On a fail the detail reads Duplicates: #id1, #id2, … for the first ten, followed by a red note, "Duplicate IDs break assistive tech and JavaScript that targets by ID.", and the full list in a code span. The "Dup IDs" tile in the Performance stats above the card shows the count and turns red at anything above zero.

The title check comes from the shared quick-check module at /js/wcag-quick-checks.js, which the Mega Analyzer and the Site Analyzer both load. It reads the text of the first <title> element, trims it, and emits one of three rows. Document title set is the pass. Document title missing (WCAG 2.4.2) is the fail, with the detail "Every page needs a <title>. Screen readers announce it first; tabs show it; search results use it." Document title suspiciously short (WCAG 2.4.2) is a warning for a trimmed title under three characters; the detail prints the title and calls it a likely placeholder. The same module emits No <main> landmark (WCAG 1.3.1) when the count is zero, N main landmarks (WCAG 1.3.1) when it is more than one, and N duplicate id attribute(s) (WCAG 4.1.1) with the first five duplicates named.

What does not trip these rows matters just as much. A <title> that reads "Home" or "Document" passes, because it is text longer than two characters; the tool can catch an empty tag, not a lazy one. A <nav> with no aria-label passes the nav row even when the page has four navs. <header> and <footer> get no row of their own. And a <main> that a framework injects on the client after hydration fails the row every time, because the analyzer only ever sees the HTML the server sent.

Why it matters

Landmarks are how a screen reader user moves around a page without tabbing through every link. The ARIA Authoring Practices Guide states the purpose directly: screen readers exploit landmark roles to provide keyboard navigation to important sections of a page. Jumping to main is the usual first move, because it clears the header, the menu and the cookie banner in one go. Without a <main> there is nothing to jump to, and a skip link that targets #content lands on a div that is not a landmark. With two <main> elements there are two candidates for that jump, and nothing in the markup says which one the user should land on.

WCAG 1.3.1 Info and Relationships is the criterion: structure conveyed through presentation can be programmatically determined or is available in text. The APG landmark guidance says each page should have one main landmark, and that when a page has more than one navigation landmark, each should carry a unique label. WAI-ARIA 1.2 says authors should mark no more than one element with the main role, and the HTML standard says a document must not have more than one <main> that is not hidden.

The document title does more jobs than any other single string on the page: the text a screen reader reads to identify the page (MDN's <title> reference says to put the page's purpose before the site name so a screen reader announces it first), the label on the browser tab, and the text Google starts from when it builds the title link. WCAG 2.4.2 Page Titled asks that pages have titles that describe topic or purpose; a missing title fails it outright, and "Document" or "Untitled" fails the spirit of it. Google's title-link documentation lists the <title> element first among its sources, then lists the reasons it will replace one: half-empty titles like "| Site Name", boilerplate repeated across a subset of pages, and titles that do not reflect what the page is about. Its best-practice list also says to avoid vague descriptors like "Home" for the home page. So a boilerplate title is a likely WCAG failure and a rewritten search result at the same time.

Duplicate ids break the plumbing accessibility depends on. The HTML standard says an id value must be unique among all the ids in the element's tree, and the lookups built on top of it assume that: the DOM standard has document.getElementById return the first element in tree order with that value, and the HTML standard resolves label for to the first element in tree order with that id. ARIA id references (aria-labelledby, aria-describedby, aria-controls) and href="#section" fragment links are id lookups too, so expect the same first-match result. When a component with a hard-coded id="search" renders once in the header and once in the footer, the footer's label points at the header's input, the footer's input has no accessible name, and any script that targets #search edits the wrong field. WCAG 4.1.1 Parsing was the criterion that named duplicate ids. WCAG 2.2 removed it, and the WCAG 2.1 Understanding document now says the criterion should be considered always satisfied for HTML. That does not make duplicates harmless; the same note says a wrong name caused by a duplicate id is covered by different success criteria, and the ones that fit are 1.3.1 (the relationship cannot be determined) and 4.1.2 Name, Role, Value (the control has no name). The quick-check module still labels the row 4.1.1; its own comment says the number was removed in 2.2 but the concern is still valid for older assistive technology.

These are the cheapest structural fixes on the whole checklist. Because a scanner can prove each of them from the HTML alone, they are also the kind of finding that gets quoted in an accessibility demand letter, in plain words like "no main landmark" and "page not titled". The ADA litigation risk post covers who sends those letters and to whom.

How to fix it

1. Landmarks. Wrap the primary content in exactly one <main id="main"> and keep the header, the navigation and the footer outside it. Give the site navigation an aria-label, and give any second nav (footer links, breadcrumbs, pagination) its own label so the landmark list can tell them apart. Keep <header> and <footer> at the body level; per the APG they define the banner and contentinfo landmarks when their context is the body element, and stop counting as landmarks when nested inside article, aside, main, nav or section. The id="main" also gives your skip link a target. The analyzer's skip-link row passes on a.skip-link, a[href="#main"] or a[href="#content"], so a skip link with a different class and target reads as missing.

<body>
  <a class="skip-link" href="#main">Skip to main content</a>
  <header>
    <nav aria-label="Primary">
      <a href="/">Home</a>
      <a href="/rooms/">Rooms</a>
    </nav>
  </header>
  <main id="main">
    <h1>Page heading</h1>
    <p>Primary content lives here and nowhere else.</p>
  </main>
  <footer>
    <nav aria-label="Footer">
      <a href="/privacy/">Privacy</a>
    </nav>
  </footer>
</body>

Use role="main" only on legacy markup where you cannot change the tag; the row treats it identically, but <main> needs no extra attribute to work. If a CMS theme wraps the content in <div class="content">, change the tag rather than bolting a role onto the div. If the analyzer reports two main landmarks, the usual cause is a page template and a component template both emitting <main>; keep the outer one and demote the inner to a <div> or a <section>.

2. Document title. Emit one <title> per page, in the shape "Page name | Site name", unique per page, long enough to name the page and short enough to survive the search result (the SERP snippet post covers the pixel widths). In a static generator, set it from front matter with a fallback to the site name and fail the build when it resolves empty. In WordPress, the title template usually lives in the SEO plugin, and a broken template variable is how "| Site Name" ends up on every page. Add the placeholders to a pre-deploy gate that reads the built output:

// pre-deploy: fail on empty or placeholder titles in the build output
import { readFileSync, globSync } from 'node:fs';
const placeholders = /^(document|untitled|home|new page|index)$/i;
let bad = 0;
for (const file of globSync('_site/**/*.html')) {
  const match = readFileSync(file, 'utf8').match(/<title>([^<]*)<\/title>/i);
  const title = (match ? match[1] : '').trim();
  if (!title || placeholders.test(title)) {
    console.log(file, JSON.stringify(title));
    bad++;
  }
}
if (bad) process.exit(1);

3. Unique ids. Start from the analyzer's duplicate list, or run the live DOM through a counter in DevTools. The DevTools version sees ids that JavaScript added after load, so it can find more than the analyzer did.

const seen = new Map();
document.querySelectorAll('[id]').forEach(el => {
  seen.set(el.id, (seen.get(el.id) || 0) + 1);
});
[...seen].filter(([, n]) => n > 1).forEach(([id, n]) => console.log(id, n));

Rename by context (nav-search, footer-search), delete ids that nothing references, and for a component that renders in a loop build the id from the loop index or the item's slug. Then re-point every reference that used the old id: label for, aria-labelledby, aria-describedby, aria-controls, href="#…", plus any #id selector in CSS and any getElementById in scripts. This is the part most sites get wrong; renaming the element without renaming the label just moves the broken relationship.

4. Verify. Re-run the Mega Analyzer and confirm the three rows and the Dup IDs tile went green. Run the WCAG Accessibility Audit for the full 70-plus-rule sweep, which adds the contrast and focus-visibility heuristics the quick set skips. Paste its failure list into the WCAG Fix Generator for a per-failure remediation prompt, and cross-check with WAVE. If your framework injects <main> on the client, the fix is to server-render the page shell, because this row and every crawler that does not execute JavaScript read the static HTML; the Prerender vs Hydration Parity tool shows what the static response is missing.

When to leave it alone

A role="main" on a div in a template you cannot restructure is fine. The row passes, role="main" is the same landmark role that <main> maps to, and swapping the tag for tidiness is not worth a regression in a theme you do not own.

A page with no navigation does not need a fake one. A standalone landing page, a receipt, a print view or an error page can honestly have no <nav>, and WCAG does not require a navigation landmark to exist. Read the nav row as "this page has no marked navigation" and decide whether that is true or an oversight; do not add an empty <nav> to turn the row green.

The HTML standard allows more than one <main> when the extras carry the hidden attribute, a pattern some single-page applications use for alternate views. The quick-check module counts every main and [role="main"] regardless of hidden, so an "N main landmarks" warning on that pattern reports the count, not a real conflict. Know that before you start deleting views.

Ids inside a separate iframe or a shadow root are not duplicates of ids in the main document; they live in different trees and the analyzer never sees them. And the fix for a duplicate is never to strip the id from the element a skip link or a form label depends on. Rename it, then rename the reference.

Fact-check notes and sources

  • Source: https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html establishes WCAG 1.3.1 Info and Relationships (Level A), the criterion that structure conveyed through presentation must be programmatically determinable, which is what landmarks satisfy.
  • Source: https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html establishes WCAG 2.4.2 Page Titled (Level A): web pages have titles that describe topic or purpose.
  • Source: https://www.w3.org/WAI/WCAG21/Understanding/parsing.html establishes WCAG 4.1.1 Parsing including the "IDs are unique" clause, carries the note that the criterion should be considered always satisfied for HTML or XML content, and says incorrect names due to a duplicate ID are covered by different success criteria.
  • Source: https://www.w3.org/TR/WCAG22/#parsing establishes that WCAG 2.2 lists 4.1.1 Parsing as obsolete and removed.
  • Source: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/ establishes that each page should have one main landmark, that multiple navigation landmarks should each have a unique label, that header and footer define banner and contentinfo when their context is the body element and not when nested in article, aside, main, nav or section, and that screen readers exploit landmark roles to provide keyboard navigation to important sections of a page.
  • Source: https://www.w3.org/TR/wai-aria-1.2/#main establishes the main role and that authors should mark no more than one element with it per document.
  • Source: https://www.w3.org/TR/html-aam-1.0/#el-main establishes that the HTML main element maps to the ARIA main role, which is why role="main" on a div and a <main> element expose the same landmark.
  • Source: https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA11 establishes the sufficient technique of using ARIA landmarks to identify regions of a page.
  • Source: https://www.w3.org/WAI/WCAG22/Techniques/html/H25 establishes the sufficient technique of providing a title using the title element.
  • Source: https://html.spec.whatwg.org/multipage/grouping-content.html#the-main-element establishes that a document must not have more than one main element that does not have the hidden attribute.
  • Source: https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute establishes that an id value must be unique among all the ids in the element's tree and must not be empty.
  • Source: https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid establishes that getElementById returns the first element in tree order whose id matches, which is why a duplicate id resolves to the first instance.
  • Source: https://html.spec.whatwg.org/multipage/forms.html#attr-label-for establishes that a label's for attribute resolves to the first element in tree order whose ID equals the attribute value, provided it is labelable.
  • Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/title establishes that the title element is shown in the browser's title bar or tab, that assistive technology users read the page title to infer content, and that putting the page's purpose before the site name lets a screen reader announce it first.
  • Source: https://developers.google.com/search/docs/appearance/title-link establishes that Google uses the title element first when generating title links, replaces half-empty, boilerplate or inaccurate titles, and recommends avoiding vague descriptors like "Home".

Related reading

If you run the same template across a dozen sites, one hard-coded id or one missing <main> in a shared component fails every property at once, and fixes once for all of them. That is the argument for owning the template rather than renting it, which is the case The $100 Network makes for multi-site operators.

This post is informational, not legal advice. Mentions of third parties are nominative fair use. No affiliation is implied.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026