# Landmarks and skip links: the structure screen readers navigate by

> header, nav, main and footer let screen reader users jump straight to the content; a skip link does the same for keyboard users. The roles, the one-main rule, the CSS, and where themes go wrong.

Updated 2026-09-25 · Accessibility · HTML version: https://getreport.app/guides/landmarks-and-skip-links

A sighted visitor finds the content of a page in a glance: the header is at the top, the menu is the row of words under the logo, the article is the big block in the middle. A screen reader user gets the same page as a stream of text, and the only way to skip the forty menu links on every page is to know where the content starts. Landmarks (`<header>`, `<nav>`, `<main>`, `<footer>`) give the page that map, and a skip link gives keyboard users a one-keystroke shortcut to the content. Both are a few tags in one template. This guide explains the roles, the rules for using them, the CSS for the skip link, and where WordPress themes and page builders lose them. Half an hour for most sites.

## Quick answer

- Wrap the page in four elements: `<header>` (logo, main menu), `<nav>` (each menu), `<main>` (the content, exactly one), `<footer>` (the bottom area).
- Give repeated landmarks names: `<nav aria-label="Main">`, `<nav aria-label="Footer">`.
- Add `<a href="#main" class="skip-link">Skip to content</a>` as the first thing in `<body>`, give `<main>` `id="main"` and `tabindex="-1"`, and hide the link until it receives focus.
- Do not put `<main>` inside `<header>`, nest one `<header>` in another, or hide the skip link with `display: none`.
- Run the [accessibility checker](https://getreport.app/tools/accessibility-checker): the landmarks and skip link findings both pass when the structure is in place.

## Why landmarks and skip links matter

### How people use them

Screen readers have a landmark navigation mode. In NVDA, pressing D jumps to the next landmark; in JAWS it is R; in VoiceOver the rotor lists them by name. A user landing on a product page presses one key, hears "main", and is at the content. Without landmarks the same user hears "banner" nothing, "main" nothing, and falls back to reading from the top or hunting for the first heading. On a page with a mega-menu, that is a hundred items before the product name.

Keyboard users who can see have the parallel problem. Tab moves through every link in order, so reaching the "Add to cart" button on a page with a 40-link header takes 40 presses, on every page. A skip link is the first Tab stop; it appears when focused and, when activated, moves focus past the header. That is one press instead of forty, and it is the first thing a keyboard user looks for.

### What the standards say

WCAG 2.4.1 (Bypass Blocks, level A) requires a way to skip repeated content; a skip link or landmarks both satisfy it, and having both is the norm. 1.3.1 (Info and Relationships) covers structure being expressed in markup rather than only visually, which is what landmarks do. Neither costs anything in design terms: the elements are invisible unless you style them.

### Why it goes missing

Landmarks disappear for boring reasons. A theme from 2014 uses `<div id="header">` and `<div id="content">`. A page builder wraps every section in nested `<div>`s with no semantic element anywhere. A React layout renders the header as a component that outputs a `<div>`. The skip link is deleted by a designer who saw it flash on Tab and thought it was a bug. None of these needs a rewrite; each is one tag or one setting.

## How getReport checks it

> **Free tool:** [Accessibility checker (WCAG 2.2)](https://getreport.app/tools/accessibility-checker): axe-core violations by impact, colour contrast, landmarks, form labels, link names, tap targets and zoom — measured on the rendered page, with the selectors to fix.

The report renders the page in Chromium and counts landmarks in the live DOM, so elements added by JavaScript count too. It counts `<header>` or `role="banner"`, `<nav>` or `role="navigation"`, `<main>` or `role="main"`, `<footer>` or `role="contentinfo"`, plus `<aside>`, `role="search"` and `<form>` for the evidence line. The finding warns when there is no `<main>` at all, or when header, nav and footer are all missing. It does not fail a page for two `<main>` elements or an unlabelled second `<nav>`; those come through as moderate axe violations instead (`landmark-one-main`, `landmark-unique`, `landmark-no-duplicate-banner`).

> **Check: Page landmarks are in place.** Screen reader users jump between header, navigation, main content and footer instead of reading top to bottom. Without these landmarks they have to listen to the whole page to find the content.
>
> 1. Wrap the page content in <main>, the site header in <header>, the menu in <nav> and the bottom area in <footer>.
> 2. If the theme uses <div id="content">, change the tag rather than adding a wrapper; most page builders offer semantic wrappers in their settings.

![The landmarks finding opened on a page built from plain divs: the title lists main and header/nav/footer as missing, the why explains that screen reader users jump between landmarks, and the fix says to change the wrapper tags rather than add new ones](https://getreport.app/guides/img/landmarks-and-skip-links/landmarks.webp "The evidence line shows the count of each landmark type the rendered page contains.")

For the skip link, the report looks at the first three focusable elements on the page. It passes when one of them is a link to an anchor on the same page (`#main`, `#content`) whose text or `aria-label` contains "skip". A skip link that is the fifth focusable element, after a cookie banner and two utility links, is not found, and a keyboard user would not find it there either.

> **Check: A skip link leads to the main content.** Keyboard users tab through every menu item on every page before they reach the content. A "Skip to content" link, visible on focus, takes them there in one keystroke.
>
> 1. Add <a href="#main" class="skip-link">Skip to content</a> as the first element in <body> and give <main> the id "main".
> 2. Hide it off-screen until it receives focus; do not use display none, which removes it from the tab order.

The moderate axe summary is where landmark quality problems appear: two banners, two `<main>`s, navigation regions without distinct names, and a `<section>` used as a region without a label:

> **Check: No moderate accessibility violations.** Moderate issues slow visitors down rather than stopping them, for example headings out of order or regions without a name. Fixing them makes the page easier for everyone to scan.
>
> 1. Open the technical detail to see each rule and the elements affected.
> 2. Fix them in the shared layout or theme so every page benefits at once.

## Step by step

### 1. Map the page to the roles

Every landmark role has an HTML element that gives it for free. Use the element; add a `role` attribute only when you cannot change the tag.

| Area of the page | Element | Role it gets | Rule |
| --- | --- | --- | --- |
| Logo, site name, main menu, search | `<header>` | `banner` | One per page. A `<header>` inside `<article>` or `<section>` is not a banner, which is correct |
| Any menu or list of links for navigation | `<nav>` | `navigation` | As many as you need; name each when there is more than one |
| The unique content of this page | `<main>` | `main` | Exactly one, not inside another landmark |
| Related but separate content: sidebar, related posts | `<aside>` | `complementary` | Name it if there are several |
| Copyright, footer links, contact details | `<footer>` | `contentinfo` | One per page; `<footer>` inside `<article>` is not a landmark |
| The site search | `<search>` or `role="search"` on the form | `search` | Wrap the search form; `<search>` is new in HTML and supported by current browsers, `role="search"` covers the rest |
| A form that matters on its own (checkout, contact) | `<form aria-label="…">` | `form` | Only a landmark when it has a name |
| A distinct section worth jumping to | `<section aria-labelledby="…">` | `region` | Only a landmark when it has a name; do not label every section |

The header and footer contain navigation, so `<nav>` goes inside them. `<main>` starts after the header and ends before the footer. Everything a visitor can see belongs in one landmark; text floating between landmarks is text a landmark user never reaches.

### 2. Change the tags in the layout template

The fix lives in one file: the theme's `header.php` and `footer.php`, the layout component in a JavaScript framework, `theme.liquid` on Shopify. Replace the wrappers; do not add new wrappers around the old ones, which leaves the CSS targeting `#header` untouched but doubles the DOM. A complete skeleton:

```html
<body>
  <a class="skip-link" href="#main">Skip to content</a>

  <header>
    <a href="https://getreport.app/"><img src="https://getreport.app/logo.svg" alt="Example Shop home" width="140" height="32"></a>
    <nav aria-label="Main">
      <ul>
        <li><a href="https://getreport.app/shoes/">Shoes</a></li>
        <li><a href="https://getreport.app/jackets/">Jackets</a></li>
      </ul>
    </nav>
    <search>
      <form action="/search/" role="search">
        <label for="q">Search</label>
        <input id="q" name="q" type="search">
        <button type="submit">Search</button>
      </form>
    </search>
  </header>

  <main id="main" tabindex="-1">
    <h1>Alpina Pro trekking shoe</h1>
    …
  </main>

  <aside aria-label="Related products">…</aside>

  <footer>
    <nav aria-label="Footer">…</nav>
    <p>© Example Shop</p>
  </footer>
</body>
```

If the CSS relies on ids or classes (`#header`, `.site-footer`), keep them on the new elements; the tag change does not affect selectors that use them. If the CSS uses the tag name `div` directly, it is time to add a class.

### 3. Name repeated landmarks

Two `<nav>` elements are announced as "navigation" and "navigation". With `aria-label` they become "Main navigation" and "Footer navigation", and a user can pick the one they want from the list. Do not include the word "navigation" in the label; the screen reader adds the role. If a visible heading already names the region, point at it instead of repeating the text:

```html
<nav aria-labelledby="footer-links-heading">
  <h2 id="footer-links-heading">Customer service</h2>
  <ul>…</ul>
</nav>
```

The same applies to several `<aside>` elements and to any `<section>` you want to expose as a region. A `<section>` without a name is not a landmark at all, which is usually what you want for the twelve sections of a landing page.

### 4. Add the skip link

The link is the first element inside `<body>`, before the header, so it is the first Tab stop. Its target is the `<main>` element's id. `tabindex="-1"` on `<main>` lets every browser move keyboard focus into it when the link is activated, so the next Tab goes to the first link in the content instead of back to the top; without it, some browsers scroll but leave focus where it was.

```html
<a class="skip-link" href="#main">Skip to content</a>
…
<main id="main" tabindex="-1">
```

The CSS keeps it off-screen until it has focus, then shows it where a sighted keyboard user will notice:

```css
.skip-link {
  position: absolute;
  top: -100%;
  left: 1rem;
  z-index: 1000;
  padding: 0.75rem 1.25rem;
  background: #0b57d0;
  color: #ffffff;
  font-weight: 600;
  text-decoration: none;
  border-radius: 0 0 4px 4px;
}

.skip-link:focus {
  top: 0;
  outline: 3px solid #ffffff;
  outline-offset: 2px;
}

/* the focused <main> should not get a focus ring of its own */
main:focus {
  outline: none;
}
```

Two things not to do: `display: none` or `visibility: hidden` on the link (both remove it from the tab order, so it never appears), and a target that is a `<div>` deep in the content rather than the `<main>` element (the jump then lands in an unpredictable place).

A sticky header can cover the target when the page scrolls to it; add `scroll-margin-top` to `main` equal to the header's height so the content lands below it.

### 5. Check the order of focus

Load the page, press Tab once. The skip link should appear. Press Enter; the next Tab should land on the first link or button inside the content. If a cookie banner opens on load and takes focus first, that is acceptable, but the skip link should still be the first Tab stop once the banner is dismissed. If the site has a fixed "back to top" or chat widget that is injected at the start of `<body>` by a script, move the skip link before it or move the widget to the end.

## Platform notes

### WordPress

**Block themes** (Twenty Twenty-Two onward and most themes built for the Site Editor) get a skip link from core: when the template contains a `<main>` element, WordPress prints a "Skip to content" link and its CSS automatically. Check the `<main>` tag exists in the Group block that wraps the content (Group block → Advanced → HTML element → `<main>`), and use the same setting to make the header and footer blocks output `<header>` and `<footer>`. Navigation blocks output `<nav>` and have an "Accessible label" setting in their block options for the `aria-label`.

**Classic themes** vary. The bundled Twenty Twenty-One and earlier all include a skip link in `header.php` and semantic wrappers. Older commercial themes often do not. Edit `header.php` and `footer.php` in a child theme: change `<div id="masthead">` to `<header id="masthead">`, `<div id="content">` to `<main id="content" tabindex="-1">`, `<div id="colophon">` to `<footer id="colophon">`, and add the skip link after the opening `<body>` tag (after `wp_body_open()`). WordPress's `.screen-reader-text` class plus a `:focus` rule does the show-on-focus part.

**Elementor.** The Hello theme wraps content in `<main id="content">` and has a skip link. Header and footer built with Theme Builder are containers, and each container has an HTML Tag setting (Layout → HTML Tag: `div`, `header`, `footer`, `main`, `article`, `section`, `aside`, `nav`). Set the header template's outer container to `header`, the footer's to `footer`, and the menu's wrapper to `nav`. Other builders (Bricks, Beaver Builder, Divi) have an equivalent setting per row or section; the report's evidence line tells you which landmarks are still at zero.

### Shopify

Dawn and the themes derived from it include a skip link (`<a class="skip-to-content-link" href="#MainContent">`), `<header>`, `<main id="MainContent">` and `<footer>` in `layout/theme.liquid`. If a customised theme lost them, that file is where they go back. Third-party apps that inject a bar at the top of `<body>` can push the skip link out of the first three focusable elements; check the order after installing one.

### Static sites and frameworks

Put the skeleton from step 2 in the layout component (`layout.tsx`, `_app`, `BaseLayout.astro`, `default.html`). In React, `<main>` is a normal element; the common mistake is a `<div id="root">` with the whole app inside a single `<div>`, and the fix is the same tag change. Single-page apps should also move focus to `<main>` after a client-side route change, so the skip link and landmark navigation work on the second page as well as the first.

## Verify

- Keyboard: load the page, press Tab once. A "Skip to content" link appears. Press Enter, then Tab: focus is on the first control inside the content.
- Screen reader: open the landmarks list (NVDA: Insert+F7 → Landmarks; VoiceOver: rotor → Landmarks). It shows one banner, one main, one content info, and each navigation with its own name. The routine is in [Testing with a screen reader](https://getreport.app/guides/testing-with-a-screen-reader).
- DevTools: in the Elements panel, search for `<main` and confirm there is exactly one. Or in the console: `document.querySelectorAll('main, [role="main"]').length` prints `1`.
- Re-run the [accessibility checker](https://getreport.app/tools/accessibility-checker): "Page landmarks are in place" and "A skip link leads to the main content" both pass, and the moderate summary lists no `landmark-*` rules.

## Common mistakes

- **Two `<main>` elements.** Symptom: axe reports `landmark-one-main`; the theme has one in the layout and a plugin template adds another. Fix: remove the inner one, or make it a `<div>`.
- **`<header>` around the whole page.** Symptom: the banner contains the main content, so "jump to main" lands inside the header. Fix: close `<header>` after the menu.
- **Skip link hidden with `display: none`.** Symptom: pressing Tab shows nothing; the report says no skip link. Fix: the off-screen CSS above.
- **Skip link target is `#content` but nothing has that id.** Symptom: the link appears, Enter does nothing. Fix: match the id to the `<main>` element.
- **Unnamed second navigation.** Symptom: the landmarks list shows "navigation" twice. Fix: `aria-label` on each.
- **Landmarks added with a wrapper `<div role="main">` around the old `<div id="content">`.** Symptom: it works, but every page carries an extra element and the CSS grows. Fix: change the tag instead. Readers who also need to fix the heading outline should see [One H1 and a sane heading order](https://getreport.app/guides/one-h1-and-a-sane-heading-order); landmarks and headings together are the page's whole map.
