Skip to content
svelte-vitals
English
Esc
navigateopen⌘Jpreview
On this page

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/contentinfo landmark at all.
  • Nesting through an intermediate, non-landmark component: if +page.svelte places <Sidebar /> inside <main>, and Sidebar.svelte itself renders role="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. It cannot see nesting introduced by an unresolvable component (node_modules, a dynamically chosen component).
  • 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 as banner/contentinfo only 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. This is an approximation: a page’s top-level <header> counts even when the layout renders the page inside <main>, where the rendered DOM would not treat it as a landmark at all, so source analysis can report a nesting the browser never exposes.
  • 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'
  }
};