a11y/top-level-landmark · Top-level landmark
A banner, main, complementary, or contentinfo landmark should not be nested inside another landmark.
Severity: warning · Category: a11y
What it checks
Flags a banner, main, complementary, or contentinfo landmark that ends up nested inside another landmark once a route’s composed layout chain (every +layout.svelte up to the route’s +page.svelte) and its resolved local components are put together.
The flagship case is a layout that renders its children inside <main> while the page contributes another landmark, e.g. an <aside>:
<header>Site navigation</header><main>{@render children()}</main>
<h1>Page content</h1><aside>Related links</aside>
No role is needed: a bare <aside> scoped to <body> or <main> is a complementary landmark. Inside sectioning content (<article>, <aside>, <nav>, <section>) it is one only when it has an aria-label or aria-labelledby, which is what the HTML accessibility mapping specifies.
Nothing in either file alone is wrong, and a file-scoped markup linter cannot see this, since the nesting only exists once the layout’s <main> and the page’s complementary are composed across files.
Not flagged:
- A route with no
banner/main/complementary/contentinfolandmark at all. - Nesting through an intermediate, non-landmark component: if
+page.svelteplaces<Sidebar />inside<main>, andSidebar.svelteitself rendersrole="complementary", that nesting is out of scope. Detection is counting-only per file, plus the direct layout-<slot>case, not a full call-graph trace through every intermediate component.
Why it matters
Assistive technology exposes banner, main, complementary, and contentinfo as landmarks a user jumps between with a keystroke, on the assumption each is a distinct, top-level region of the page. A landmark nested inside another loses that meaning: it disappears from landmark navigation, or reads as content of the outer landmark instead of the region it’s meant to be.
How to fix
Move the nested landmark out so every landmark composes at the top level of the route:
<header>Site navigation</header><main>{@render children()}</main>
<h1>Page content</h1>
<aside>Related links</aside>
<!-- moved out from under +layout.svelte's <main> -->
Mode differences
Landmarks are collected in both modes, but from different sources, so results can differ:
- Source analysis (the CLI, the dashboard’s static baseline) composes the route’s layout chain with its resolved local components to detect nesting. A layout landmark around the page counts only where the page’s landmark sits in an arm that can hold together with the layout’s arm, reading a field of the
loaddata alike across the chain (as for a11y/duplicate-landmark). It cannot see nesting introduced by an unresolvable component (node_modules, a component picked from data or a lookup table at runtime). - Source analysis (the CLI, the dashboard’s static baseline) also decides whether a
<header>/<footer>is a landmark by where it sits in its own file: it counts asbanner/contentinfoonly at the template top level of a chain file (a layout or the page), never inside a component, whose header may sit under sectioning content the component cannot see. A chain file the layout above renders inside<main>or<aside>is scoped by it, so its top-level<header>/<footer>is not a landmark there either. This is an approximation: a layout that wraps the page in<article>,<section>or<nav>instead is not seen, so a page’s top-level<header>still counts there, although the rendered DOM would not treat it as a landmark. A<header>/<footer>inside sectioning content, in its own file or through the layout’s<main>/<aside>, does not contain a landmark either: an<aside>inside it is reported as nested in the landmark around it, if any. - Rendered analysis (the Vite plugin’s build pass, a route you visit in the dashboard) reads the rendered HTML, so it sees nesting produced by any component, resolvable or not. It has no source files to attribute a finding to, so its findings anchor to the route itself rather than a specific file and line. The persisted finding key differs from the source-analysis key for the same defect.
When the two disagree, trust the rendered result. It reflects what ships to the browser.
Disabling
An inline svelte-vitals-disable-next-line comment above the line the finding names silences it, in source analysis only: a build-pass finding points at the prerendered HTML and has no source line to sit above. That line often sits in a composed component, and one directive there silences the finding on every route composing it. The suppressions file (npx svelte-vitals --update-suppressions) is the per-route mechanism. You can also scope the rule per route or path with overrides, or turn it off:
export default {
rules: {
'a11y/top-level-landmark': 'off'
}
};