Landmarks
ページの主要セクションを識別する8つのARIAランドマークロールのセット。支援技術ユーザーのナビゲーションを効率化します。
🤖 AI Implementation Guideデモ
このデモでは8つのランドマーク領域を色分けされたボーダーで視覚化しています。各ランドマークにはロールを識別するためのラベルが表示されています。
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
ネイティブHTMLセマンティック要素を優先
可能な限りネイティブHTMLセマンティック要素を使用してください。 これらはARIA属性なしでランドマークロールを提供し、支援技術によるサポートも優れています。
<header>...</header> <!-- banner -->
<nav>...</nav> <!-- navigation -->
<main>...</main> <!-- main -->
<footer>...</footer> <!-- contentinfo -->
<aside>...</aside> <!-- complementary -->
<section aria-label="...">...</section> <!-- region --> | HTML要素 | ARIAロール | 自動マッピング条件 |
|---|---|---|
<header> | banner | <body>の直接の子の場合のみ |
<nav> | navigation | 常に |
<main> | main | 常に |
<footer> | contentinfo | <body>の直接の子の場合のみ |
<aside> | complementary | 常に |
<section> | region | aria-labelまたはaria-labelledbyがある場合のみ |
<form> | form | aria-labelまたはaria-labelledbyがある場合のみ |
注:検索機能には<form role="search">を使用してください。<search>要素はブラウザサポートが限定的です。
アクセシビリティ
WAI-ARIA ランドマークロール
ランドマークはページの主要なセクションを識別します。8つのランドマークロールがあり、支援技術ユーザーがページ構造を効率的にナビゲートできるようになります。
| ロール | HTML要素 | 説明 | 制約 |
|---|---|---|---|
banner | <header> | サイト全体のヘッダー | ページに1つ、トップレベルのみ |
navigation | <nav> | ナビゲーションリンク | 複数可、2つ以上の場合はラベル必須 |
main | <main> | 主要コンテンツ | ページに1つ、トップレベルのみ |
contentinfo | <footer> | サイト全体のフッター | ページに1つ、トップレベルのみ |
complementary | <aside> | 補完的コンテンツ | トップレベル推奨 |
region | <section> | 名前付きセクション | aria-label/labelledby必須 |
search | <form role="search"> | 検索機能 | form要素と併用 |
form | <form> | フォーム領域 | aria-label/labelledby必須 |
WAI-ARIA プロパティ
aria-label / aria-labelledby
ランドマークにアクセシブルな名前を提供します。同じ種類のランドマークが複数存在する場合、またはregionとformランドマークには必須です。
| 値 | 文字列(aria-label)またはID参照(aria-labelledby) |
| 必須条件 |
|
| 例 | <nav aria-label="メイン"> または
<section aria-labelledby="heading-id"> |
キーボードサポート
ランドマークにはキーボード操作は不要です。ランドマークは構造要素であり、インタラクティブなウィジェットではありません。スクリーンリーダーは組み込みのランドマークナビゲーションを提供します:
| スクリーンリーダー | ナビゲーション方法 |
|---|---|
| NVDA | Dキー |
| VoiceOver | ローター(Webナビゲーション) |
| JAWS | Rキーまたはセミコロン |
ベストプラクティス
- セマンティックHTML要素を使用 - ARIAロールよりも
<header>、<nav>、<main>などを優先 - 複数のランドマークにラベルを付ける - 複数の
<nav>や<aside>がある場合、それぞれに一意のアクセシブルな名前を付ける - ランドマークの数を制限 - ランドマークが多すぎる(約7以上)と支援技術ユーザーにとって煩わしくなる
- すべてのコンテンツをランドマーク内に配置 - ランドマーク外のコンテンツは、ランドマークでナビゲートするユーザーに見落とされる可能性がある
- トップレベルに配置 -
<header>と<footer>は<body>の直接の子の場合にのみbanner/contentinfoにマップされる
参考資料
ソースコード
<script 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/
*/
/** Show landmark labels overlay */
export let showLabels: boolean = false;
let className: string = '';
export { className as class };
</script>
<div class="apg-landmark-demo {className}" {...$$restProps}>
<!-- Banner Landmark -->
<header class="apg-landmark-banner">
<span class="apg-landmark-label" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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" class:hidden={!showLabels} 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> テスト
テストでは、8つのランドマークロールがすべて存在し、適切にラベル付けされ、APGの制約に従っていることを確認します。ランドマークは静的な構造要素であるため、テストはインタラクションよりもHTML構造に焦点を当てています。
テスト戦略
ユニットテスト(Container API / Testing Library)
コンポーネントのHTML出力とランドマーク構造を検証します。ブラウザなしで正しいレンダリングを確認できます。
- 8つのランドマークロールがすべて存在
- セマンティックHTML要素の使用
- 適切なラベル付け(aria-label/aria-labelledby)
- 制約の検証(mainは1つのみ、bannerは1つのみなど)
E2Eテスト(Playwright)
実際のブラウザ環境でランドマーク構造を検証し、アクセシビリティ監査を実行します。
- ランドマークロールが正しく公開される
- axe-coreアクセシビリティ監査
- フレームワーク間の一貫性
テストカテゴリ
高優先度: ランドマーク構造
| テスト | 説明 |
|---|---|
banner ランドマーク | bannerロールを持つheader要素がある |
navigation ランドマーク | navigationロールを持つnav要素がある |
main ランドマーク | mainロールを持つmain要素がある |
contentinfo ランドマーク | contentinfoロールを持つfooter要素がある |
complementary ランドマーク | complementaryロールを持つaside要素がある |
region ランドマーク | aria-labelledbyを持つsection(regionロール)がある |
search ランドマーク | role="search"を持つformがある |
form ランドマーク | aria-label(formロール)を持つformがある |
mainは1つのみ | mainランドマークが1つだけ存在する |
bannerは1つのみ | bannerランドマークが1つだけ存在する |
contentinfoは1つのみ | contentinfoランドマークが1つだけ存在する |
高優先度: ランドマークのラベル付け
| テスト | 説明 |
|---|---|
navigationのラベル | 複数のnavigationが一意のaria-labelを持つ |
regionのラベル | regionがaria-labelまたはaria-labelledbyを持つ |
formのラベル | formランドマークがaria-labelを持つ |
searchのラベル | searchランドマークがaria-labelを持つ |
complementaryのラベル | complementaryランドマークがaria-labelを持つ |
中優先度: アクセシビリティ
| テスト | 説明 |
|---|---|
axe-core監査 | WCAG 2.1 AAの違反がない |
低優先度: プロパティと構造
| テスト | 説明 |
|---|---|
className プロパティ | カスタムclassNameがコンテナに適用される |
showLabels プロパティ | 有効時にランドマークラベルが表示される |
セマンティックHTML | header、nav、main、footer、aside、section要素を使用 |
チェックリスト(手動検証)
これらの推奨事項は手動または専用ツールで検証するのが最適です:
- すべてのコンテンツがランドマーク内にある(推奨だが必須ではない)
- ランドマークの総数が7以下(多すぎると煩雑になる)
- スクリーンリーダーナビゲーションが正しく動作する(NVDA D キー、VoiceOver ローター)
テストツール
- Vitest (opens in new tab) - ユニットテスト用テストランナー
- Astro Container API (opens in new tab) - ユニットテスト用サーバーサイドコンポーネントレンダリング
- Playwright (opens in new tab) - E2Eテスト用ブラウザ自動化
- axe-core (opens in new tab) - アクセシビリティテストエンジン
詳細は testing-strategy.md (opens in new tab) を参照してください。
import { render, screen, within } from '@testing-library/svelte';
import { axe } from 'jest-axe';
import { describe, expect, it } from 'vitest';
import LandmarkDemo from './LandmarkDemo.svelte';
describe('LandmarkDemo (Svelte)', () => {
// 🔴 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', '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();
});
});
}); リソース
- WAI-ARIA APG: ランドマークパターン (opens in new tab)
- WAI-ARIA APG: ランドマーク領域 (opens in new tab)
- MDN: ARIA ランドマークロール (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist