Landmarks
A set of eight ARIA landmark roles that identify the major sections of a page for assistive technology users.
🤖 AI Implementation GuideDemo
This demo visualizes the 8 landmark regions with distinct colored borders. Each landmark is labeled to help identify its role.
Main Content
This section demonstrates the region landmark. A section element only becomes a region landmark when it has an accessible name via aria-labelledby or aria-label.
Native HTML
Prefer Native HTML Semantic Elements
Always use native HTML semantic elements when possible. They provide built-in landmark roles without needing ARIA attributes and are better supported by assistive technologies.
<header>...</header> <!-- banner -->
<nav>...</nav> <!-- navigation -->
<main>...</main> <!-- main -->
<footer>...</footer> <!-- contentinfo -->
<aside>...</aside> <!-- complementary -->
<section aria-label="...">...</section> <!-- region --> | HTML Element | ARIA Role | Auto-mapping Condition |
|---|---|---|
<header> | banner | Only when direct child of <body> |
<nav> | navigation | Always |
<main> | main | Always |
<footer> | contentinfo | Only when direct child of <body> |
<aside> | complementary | Always |
<section> | region | Only when aria-label or aria-labelledby is present |
<form> | form | Only when aria-label or aria-labelledby is present |
Note: For search functionality, use <form role="search"> as the <search> element has limited browser support.
Accessibility Features
WAI-ARIA Landmark Roles
Landmarks identify the major sections of a page. There are 8 landmark roles that enable assistive technology users to efficiently navigate page structure.
| Role | HTML Element | Description | Constraint |
|---|---|---|---|
banner | <header> | Site-wide header | One per page, top-level only |
navigation | <nav> | Navigation links | Multiple allowed, label when >1 |
main | <main> | Primary content | One per page, top-level only |
contentinfo | <footer> | Site-wide footer | One per page, top-level only |
complementary | <aside> | Supporting content | Top-level recommended |
region | <section> | Named section | Requires aria-label/labelledby |
search | <form role="search"> | Search functionality | Use with form element |
form | <form> | Form area | Requires aria-label/labelledby |
WAI-ARIA Properties
aria-label / aria-labelledby
Provides an accessible name for the landmark. Required when multiple landmarks of the same
type exist, or for region and form landmarks.
| Values | String (aria-label) or ID reference (aria-labelledby) |
| Required |
|
| Example | <nav aria-label="Main"> or
<section aria-labelledby="heading-id"> |
Keyboard Support
Landmarks require no keyboard interaction. They are structural elements, not interactive widgets. Screen readers provide built-in landmark navigation:
| Screen Reader | Navigation |
|---|---|
| NVDA | D key |
| VoiceOver | Rotor (Web navigation) |
| JAWS | R key or semicolon |
Best Practices
- Use semantic HTML elements - Prefer
<header>,<nav>,<main>, etc. over ARIA roles - Label multiple landmarks - When you have more than one
<nav>or<aside>, give each a unique accessible name - Limit landmark count - Too many landmarks (more than ~7) can be overwhelming for assistive technology users
- Place all content in landmarks - Content outside landmarks may be missed by users navigating by landmarks
- Top-level placement -
<header>and<footer>only map tobanner/contentinfowhen they are direct children of<body>
References
Source Code
<script setup lang="ts">
/**
* LandmarkDemo - Demonstrates the 8 ARIA landmark roles
*
* This component visualizes proper landmark structure for educational purposes.
*
* @see https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/
*/
defineOptions({
inheritAttrs: true,
});
interface Props {
/** Show landmark labels overlay */
showLabels?: boolean;
class?: string;
}
const props = withDefaults(defineProps<Props>(), {
showLabels: false,
});
</script>
<template>
<div :class="['apg-landmark-demo', props.class]">
<!-- Banner Landmark -->
<header class="apg-landmark-banner">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>banner</span
>
<div class="apg-landmark-content">
<span class="apg-landmark-logo">Site Logo</span>
<!-- Navigation Landmark (Main) -->
<nav aria-label="Main" class="apg-landmark-navigation">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>navigation</span
>
<ul class="apg-landmark-nav-list">
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</div>
</header>
<!-- Main Landmark -->
<main class="apg-landmark-main">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true">main</span>
<div class="apg-landmark-main-content">
<!-- Region Landmark -->
<section aria-labelledby="content-heading" class="apg-landmark-region">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>region</span
>
<h2 id="content-heading" class="apg-landmark-heading">Main Content</h2>
<p>
This section demonstrates the <code>region</code> landmark. A section element only
becomes a region landmark when it has an accessible name via
<code>aria-labelledby</code> or <code>aria-label</code>.
</p>
<!-- Search Landmark -->
<form role="search" aria-label="Site search" class="apg-landmark-search">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>search</span
>
<label for="search-input" class="apg-landmark-search-label">Search</label>
<input
type="search"
id="search-input"
class="apg-landmark-search-input"
placeholder="Search..."
/>
<button type="submit" class="apg-landmark-search-button">Search</button>
</form>
<!-- Form Landmark -->
<form aria-label="Contact form" class="apg-landmark-form">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>form</span
>
<div class="apg-landmark-form-field">
<label for="name-input">Name</label>
<input type="text" id="name-input" class="apg-landmark-input" />
</div>
<div class="apg-landmark-form-field">
<label for="email-input">Email</label>
<input type="email" id="email-input" class="apg-landmark-input" />
</div>
<button type="submit" class="apg-landmark-submit">Submit</button>
</form>
</section>
<!-- Complementary Landmark -->
<aside aria-label="Related content" class="apg-landmark-complementary">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>complementary</span
>
<h2 class="apg-landmark-heading">Related</h2>
<ul>
<li><a href="#related1">Related Link 1</a></li>
<li><a href="#related2">Related Link 2</a></li>
</ul>
</aside>
</div>
</main>
<!-- Contentinfo Landmark -->
<footer class="apg-landmark-contentinfo">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>contentinfo</span
>
<div class="apg-landmark-content">
<!-- Navigation Landmark (Footer) -->
<nav aria-label="Footer" class="apg-landmark-navigation">
<span :class="['apg-landmark-label', !showLabels && 'hidden']" aria-hidden="true"
>navigation</span
>
<ul class="apg-landmark-nav-list">
<li><a href="#privacy">Privacy</a></li>
<li><a href="#terms">Terms</a></li>
<li><a href="#sitemap">Sitemap</a></li>
</ul>
</nav>
<p class="apg-landmark-copyright">© 2024 Example Site</p>
</div>
</footer>
</div>
</template> Testing
Tests verify that all 8 landmark roles are present, properly labeled, and follow APG constraints. Since landmarks are static structural elements, testing focuses on HTML structure rather than interaction.
Testing Strategy
Unit Tests (Container API / Testing Library)
Verify the component's HTML output and landmark structure. These tests ensure correct rendering without requiring a browser.
- All 8 landmark roles are present
- Semantic HTML elements are used
- Proper labeling (aria-label/aria-labelledby)
- Constraint validation (one main, one banner, etc.)
E2E Tests (Playwright)
Verify landmark structure in a real browser environment and run accessibility audits.
- Landmark roles are exposed correctly
- axe-core accessibility audit
- Cross-framework consistency
Test Categories
High Priority: Landmark Structure
| Test | Description |
|---|---|
banner landmark | Has header element with banner role |
navigation landmark | Has nav element(s) with navigation role |
main landmark | Has main element with main role |
contentinfo landmark | Has footer element with contentinfo role |
complementary landmark | Has aside element with complementary role |
region landmark | Has section with aria-labelledby (region role) |
search landmark | Has form with role="search" |
form landmark | Has form with aria-label (form role) |
exactly one main | Only one main landmark exists |
exactly one banner | Only one banner landmark exists |
exactly one contentinfo | Only one contentinfo landmark exists |
High Priority: Landmark Labeling
| Test | Description |
|---|---|
navigation labels | Multiple navigations have unique aria-labels |
region label | Region has aria-label or aria-labelledby |
form label | Form landmark has aria-label |
search label | Search landmark has aria-label |
complementary label | Complementary landmark has aria-label |
Medium Priority: Accessibility
| Test | Description |
|---|---|
axe-core audit | No WCAG 2.1 AA violations |
Low Priority: Props & Structure
| Test | Description |
|---|---|
className prop | Custom className is applied to container |
showLabels prop | Landmark labels are visible when enabled |
semantic HTML | Uses header, nav, main, footer, aside, section elements |
Checklist (Manual Verification)
These recommendations are best verified manually or with specialized tools:
- All content is within landmarks (recommended but not required)
- Total landmarks ≤ 7 (too many can be overwhelming)
- Screen reader navigation works correctly (NVDA D key, VoiceOver rotor)
Testing Tools
- Vitest (opens in new tab) - Test runner for unit tests
- Astro Container API (opens in new tab) - Server-side component rendering for unit tests
- Playwright (opens in new tab) - Browser automation for E2E tests
- axe-core (opens in new tab) - Accessibility testing engine
See testing-strategy.md (opens in new tab) for full documentation.
import { render, screen, within } from '@testing-library/vue';
import { axe } from 'jest-axe';
import { describe, expect, it } from 'vitest';
import LandmarkDemo from './LandmarkDemo.vue';
describe('LandmarkDemo (Vue)', () => {
// 🔴 High Priority: Landmark Roles
describe('APG: Landmark Roles', () => {
it('has banner landmark (header)', () => {
render(LandmarkDemo);
expect(screen.getByRole('banner')).toBeInTheDocument();
});
it('has navigation landmark (nav)', () => {
render(LandmarkDemo);
expect(screen.getAllByRole('navigation').length).toBeGreaterThanOrEqual(1);
});
it('has main landmark (main)', () => {
render(LandmarkDemo);
expect(screen.getByRole('main')).toBeInTheDocument();
});
it('has contentinfo landmark (footer)', () => {
render(LandmarkDemo);
expect(screen.getByRole('contentinfo')).toBeInTheDocument();
});
it('has complementary landmark (aside)', () => {
render(LandmarkDemo);
expect(screen.getByRole('complementary')).toBeInTheDocument();
});
it('has region landmark with label', () => {
render(LandmarkDemo);
const region = screen.getByRole('region');
expect(region).toBeInTheDocument();
expect(region).toHaveAccessibleName();
});
it('has search landmark', () => {
render(LandmarkDemo);
expect(screen.getByRole('search')).toBeInTheDocument();
});
it('has form landmark with label', () => {
render(LandmarkDemo);
const form = screen.getByRole('form');
expect(form).toBeInTheDocument();
expect(form).toHaveAccessibleName();
});
it('has exactly one main landmark', () => {
render(LandmarkDemo);
expect(screen.getAllByRole('main')).toHaveLength(1);
});
it('has exactly one banner landmark', () => {
render(LandmarkDemo);
expect(screen.getAllByRole('banner')).toHaveLength(1);
});
it('has exactly one contentinfo landmark', () => {
render(LandmarkDemo);
expect(screen.getAllByRole('contentinfo')).toHaveLength(1);
});
});
// 🔴 High Priority: Landmark Labeling
describe('APG: Landmark Labeling', () => {
it('navigation landmarks have unique labels when multiple', () => {
render(LandmarkDemo);
const navs = screen.getAllByRole('navigation');
if (navs.length > 1) {
const labels = navs.map(
(nav) => nav.getAttribute('aria-label') || nav.getAttribute('aria-labelledby')
);
const uniqueLabels = new Set(labels.filter(Boolean));
expect(uniqueLabels.size).toBe(navs.length);
}
});
it('region landmark has accessible name', () => {
render(LandmarkDemo);
const region = screen.getByRole('region');
const hasLabel = region.hasAttribute('aria-label') || region.hasAttribute('aria-labelledby');
expect(hasLabel).toBe(true);
});
it('form landmark has accessible name', () => {
render(LandmarkDemo);
const form = screen.getByRole('form');
const hasLabel = form.hasAttribute('aria-label') || form.hasAttribute('aria-labelledby');
expect(hasLabel).toBe(true);
});
it('search landmark has accessible name', () => {
render(LandmarkDemo);
const search = screen.getByRole('search');
const hasLabel = search.hasAttribute('aria-label') || search.hasAttribute('aria-labelledby');
expect(hasLabel).toBe(true);
});
it('complementary landmark has accessible name', () => {
render(LandmarkDemo);
const aside = screen.getByRole('complementary');
const hasLabel = aside.hasAttribute('aria-label') || aside.hasAttribute('aria-labelledby');
expect(hasLabel).toBe(true);
});
});
// 🟡 Medium Priority: Accessibility
describe('Accessibility', () => {
it('has no axe-core violations', async () => {
const { container } = render(LandmarkDemo);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
// 🟢 Low Priority: Props
describe('Props', () => {
it('applies class to container', () => {
render(LandmarkDemo, {
props: { class: 'custom-class' },
attrs: { 'data-testid': 'landmark-demo' },
});
const container = screen.getByTestId('landmark-demo');
expect(container).toHaveClass('custom-class');
});
it('shows landmark labels when showLabels is true', () => {
render(LandmarkDemo, {
props: { showLabels: true },
});
expect(screen.getByText('banner')).toBeInTheDocument();
expect(screen.getByText('main')).toBeInTheDocument();
expect(screen.getByText('contentinfo')).toBeInTheDocument();
});
});
// 🟢 Low Priority: Semantic Structure
describe('Semantic Structure', () => {
it('uses semantic HTML elements', () => {
const { container } = render(LandmarkDemo);
expect(container.querySelector('header')).toBeInTheDocument();
expect(container.querySelector('nav')).toBeInTheDocument();
expect(container.querySelector('main')).toBeInTheDocument();
expect(container.querySelector('footer')).toBeInTheDocument();
expect(container.querySelector('aside')).toBeInTheDocument();
expect(container.querySelector('section')).toBeInTheDocument();
});
it('main contains primary content area', () => {
render(LandmarkDemo);
const main = screen.getByRole('main');
expect(within(main).getByRole('region')).toBeInTheDocument();
expect(within(main).getByRole('complementary')).toBeInTheDocument();
});
});
}); Resources
- WAI-ARIA APG: Landmarks Pattern (opens in new tab)
- WAI-ARIA APG: Landmark Regions (opens in new tab)
- MDN: ARIA Landmark Roles (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist