Skip Links and Landmarks in SSG Layouts
A documentation page has a lot of furniture before the content: a header with search and a theme toggle, a top navigation bar, a sidebar with dozens or hundreds of links, breadcrumbs. A sighted mouse user ignores it. A keyboard user has to tab through it on every page; a screen reader user hears it unless the page gives them a way to jump. Two mechanisms solve this, for two different groups of readers. A skip link is a visible-on-focus link at the top of the page that jumps to the main content, for keyboard users. Landmarks are the structural elements — <header>, <nav>, <main>, <aside>, <footer> — that screen readers list and jump between with a single key.
Both live in the base layout, so fixing them once fixes every page. This guide adds them to layouts in Astro, Eleventy, Hugo and Jekyll, using the reference site from Accessibility for Static Sites to measure the result.
Prerequisites
- Access to the generator's base layout:
src/layouts/Base.astro,_includes/base.njk,layouts/_default/baseof.htmlor_layouts/default.html. - A screen reader to verify: NVDA on Windows (free) or VoiceOver on macOS.
- axe-core or Lighthouse for the automated part of the check.
What Was Wrong
The reference site's marketing section used a hand-built Astro layout; the docs section used Starlight, which already had both mechanisms. An NVDA landmarks list (Insert+F7) on a marketing page showed:
| Expected landmark | Present? | Problem |
|---|---|---|
banner (<header>) | Yes | — |
| navigation, "Main" | Yes | Unnamed, identical to footer nav |
| main | No | Content in <div id="content"> |
complementary (<aside>) | No | Sidebar in <div class="sidebar"> |
contentinfo (<footer>) | Yes | Contained a second unnamed <nav> |
| Skip link | Present | Pointed at #main, which did not exist |
The skip link is the instructive failure. It existed, looked right when focused, and did nothing: the template had been refactored, the ID moved, and nothing checked that the target still existed.
The Markup, Generator by Generator
The structure is the same everywhere. Only the template syntax changes.
Astro (src/layouts/Base.astro):
---
const { title } = Astro.props;
---
<html lang="en">
<body>
<a class="skip-link" href="#content">Skip to content</a>
<header>
<nav aria-label="Main"><slot name="nav" /></nav>
</header>
<main id="content" tabindex="-1"><slot /></main>
<aside aria-label="Related"><slot name="aside" /></aside>
<footer><nav aria-label="Footer"><slot name="footer" /></nav></footer>
</body>
</html>
Eleventy (_includes/base.njk) uses the same elements with {% include "nav.njk" %} for the slots and {{ content | safe }} inside <main>. Hugo (layouts/_default/baseof.html) places {{ block "main" . }}{{ end }} inside <main id="content" tabindex="-1">. Jekyll (_layouts/default.html) puts {{ content }} there. In all four the skip link is the first element in <body>, before the header, so it is the first Tab stop.
Styling the Skip Link
The link should be invisible until focused and fully visible when it is. Clipping keeps it in the tab order; display: none would not:
.skip-link {
position: absolute; left: 0.5rem; top: 0.5rem; z-index: 100;
padding: 0.5rem 1rem; background: var(--surface); color: var(--text);
border: 2px solid var(--focus-ring); border-radius: 4px;
clip-path: inset(50%); white-space: nowrap;
}
.skip-link:focus { clip-path: none; }
The tabindex="-1" on <main> matters more than it looks. Without it, activating the skip link in Safari scrolls the page but leaves the sequential focus point at the top; the next Tab press goes to the first header link again. With it, focus moves to <main> and the next Tab press goes to the first link in the article. -1 makes the element focusable by script and by fragment navigation without adding it to the tab order.
Naming Repeated Landmarks
A page with one <nav> needs no name. A documentation page usually has four: main navigation, sidebar, table of contents and breadcrumbs, often plus a footer navigation. Each needs aria-label so a screen reader lists "Main navigation", "Docs sidebar navigation", "On this page navigation" rather than five identical entries. Do not include the word "navigation" in the label; the screen reader adds the role, and "Main navigation navigation" is what users hear otherwise.
The same applies to <aside>: name it if there is more than one, and do not wrap the whole sidebar navigation in <aside> and <nav> with separate names unless they really are separate regions. On the reference site the docs sidebar is <nav aria-label="Docs sidebar"> with no <aside> around it, and the "Related" box at the end of articles is <aside aria-label="Related">.
Checking It Stays Correct
Two checks keep this from rotting the way the old skip link did:
- axe-core rules
landmark-one-main,region,landmark-uniqueandskip-linkrun against every layout in CI.skip-linkfails when a skip link's target does not exist — exactly the regression that had shipped. - A link check on fragments. The site's link checker validates
#fragmenttargets on internal links, including the skip link, as in Testing Redirects and Headers Before Deploy, which uses the same crawler.
A manual check with the screen reader's landmark list takes a minute per layout and is part of the template-change checklist.
Documentation Frameworks: What to Leave Alone
Starlight, VitePress and Docusaurus already ship a skip link and named landmarks, and the most common way teams break them is by overriding the layout. On the reference site, the docs section stayed clean because custom components were added through Starlight's component overrides — Header, Sidebar, PageFrame — which keep the outer landmark structure intact. The one regression in the docs section came from a custom PageFrame that wrapped the main content in an extra <div role="main"> "for styling", which produced two main landmarks and an axe landmark-no-duplicate-main failure on every page. Styling hooks never need a role; a plain class is enough.
The same caution applies when a documentation framework is embedded in a larger site. If the marketing header is rendered around a VitePress or Docusaurus page, each layer tends to bring its own <header> and skip link, and the page ends up with two banners and two skip links pointing at different targets. Decide which layer owns the outer structure — usually the framework, because it controls <main> — and have the other render plain markup inside it.
Landmarks for Search and Dialogs
Two more regions are worth naming on a documentation site. A search form should be wrapped in <search> (now supported in all current browsers) or a <form role="search">, so screen reader users can jump straight to it from the landmarks list; on the reference site, search was the second most used landmark in a short survey of four screen reader users, after main. Modal dialogs, such as a search overlay, should not be landmarks at all while closed: a <dialog> element that is not open is hidden from the accessibility tree automatically, while a <div> styled as a hidden overlay often is not, and shows up in the landmark list as an empty region. The search dialog pattern in Accessible Search UI for Static Sites uses <dialog> for that reason.
Measured Impact
| Measure | Before | After | Tool |
|---|---|---|---|
| Landmarks on marketing pages | 4 (2 unnamed duplicates) | 6, all named | NVDA landmarks list |
axe region violations (60 pages) | 41 elements | 0 | axe-core 4.10 |
axe landmark-one-main violations | 22 pages | 0 | axe-core 4.10 |
| Working skip link | No | Yes | Keyboard, Safari and Firefox |
| Time to reach article, keyboard | 41 s | 2 s | Stopwatch, median of 5 |
Pitfalls & Rollback
- Hiding the skip link with
display: none. It must stay focusable; clip it instead. - Skip link target without
tabindex="-1". Focus does not move in all browsers. - Unnamed duplicate landmarks. Name every
<nav>when there is more than one. - Putting the skip link after the header. It must be the first focusable element.
- Rollback: the changes are in one layout file and one CSS block; reverting the commit restores the old layout, and the axe rules will report the regression immediately.
Conclusion
A skip link and a full set of named landmarks give keyboard and screen reader users a way past the navigation that every static site repeats on every page. Both belong in the base layout, take under an hour to add in any generator, and need two automated checks — axe's landmark rules and a fragment-aware link check — so that the next refactor cannot quietly break them.
FAQ
Does a skip link still matter if the page has landmarks?
Yes. Landmarks help screen reader users, who can jump between them with a shortcut key, but sighted keyboard users have no landmark shortcut. For them the skip link is the only way past a long header and sidebar. The two serve different groups of people and both are cheap to add.
Should the skip link be hidden until it receives focus?
That is the common and acceptable pattern, as long as it becomes fully visible on focus. Hide it with a clipping technique, not display none, which would remove it from the tab order. Some teams leave it permanently visible, which is also fine and avoids the question entirely.
Why does my skip link scroll but leave focus at the top?
The target is not focusable, so some browsers move the viewport but not the sequential focus starting point. Add tabindex minus one to the main element so the browser can move focus to it, and the next Tab press continues from the article rather than from the top of the page.
How many nav landmarks can a page have?
As many as it needs, but each one must have a distinct accessible name through aria-label or aria-labelledby, such as Main, Docs sidebar, On this page and Breadcrumb. Unnamed duplicate navigation landmarks are announced identically, so a screen reader user cannot tell them apart.
Related
- Parent: Accessibility for Static Sites — where layout fits among the other layers.
- Keyboard Navigation and Focus Order on Static Sites — focus styles and tab order beyond the skip link.
- Fixing Heading Order Problems in Markdown Content — the other structure screen reader users navigate by.
- Sidebar Navigation in Astro and Eleventy — building the sidebar the skip link jumps past.
- Evaluating SSG Accessibility Defaults — which themes ship these already.
Filed underHow-tos