anchor-links について
anchor-links は、ページ内ナビゲーションやスキップリンクを含むページを作成・評価するためのアクセシビリティスキルです。リンクの意味、フラグメントの移動先、フォーカス、動きの設定、テストを扱います。
主な機能
- 01リンクテキストとナビゲーションの意味を確認
- 02一意で表示可能なフラグメント先を確認
- 03スキップリンクとフォーカス管理を案内
- 04固定ヘッダーと動きの軽減設定に対応
- 05キーボード、スクリーンリーダー、自動テストを案内
活用シーン
- 01目次リンクや見出しリンクの評価
- 02キーボードでスキップリンクを確認
- 03固定ヘッダーに隠れる移動先を特定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mgifford/ accessibility-skills --skill anchor-linksインストール後、Claude Code で/anchor-linksを入力して読み込まれたか確認します。
同梱ファイルSYNC.md
詳細
アンカーリンクのアクセシビリティスキル
正式な参照元:
mgifford/ACCESSIBILITY.mdのexamples/ANCHOR_LINKS_ACCESSIBILITY_BEST_PRACTICES.mdこのスキルはそのファイルに基づいています。判断に迷った場合は、例の内容を優先してください。
ページ内アンカーリンク、スキップリンク、見出しリンクを作成またはレビューする際は、これらのルールを適用してください。 プロジェクトにページ内ナビゲーションまたはスキップリンクがある場合にのみ、このスキルを読み込んでください。
中核となる要件
すべてのアンカーリンクには、意味のあるテキスト、到達可能でフォーカスインジケーターが見えるターゲットを設定し、動きによる悪影響を引き起こさないようにしてください。ナビゲーションには href を持つ本物の <a> 要素を使用し、ナビゲーションを伴わない操作にはボタンを使用してください。JavaScript を追加する前に、ネイティブのフラグメントナビゲーションを優先してください。
重大度の基準(このスキル)
| レベル | 意味 |
|---|---|
| Critical | アンカーナビゲーションによってアクセスが妨げられる、またはトラップが発生する |
| Serious | スキップリンクが機能しない、または恒久的に非表示になっている。スムーズスクロールによって前庭系に害が生じる |
| Moderate | 固定ヘッダーによってフォーカスインジケーターが隠れる。リンクテキストが文脈上あいまいである |
| Minor | スラッグ形式ではない ID。目次に aria-current がない |
ナビゲーションにはリンクを、操作にはボタンを使用する
有効化によって URL が変わる、または場所へ移動する場合はアンカーを使用し、ナビゲーションを伴わずに操作を実行する場合はボタンを使用してください。
<a href="#installation">Installation instructions</a>
<button type="button" id="copy-installation-link">Copy link to Installation</button>
操作にダミーリンクを使用しないでください。
<a href="#">Open settings</a>
<a href="javascript:void(0)">Open settings</a>
空のフラグメントはドキュメントの先頭へ移動します。JavaScript URL は、通常のリンクのセマンティクス(履歴、コピー、新しいタブで開く、JavaScript が無効な場合のフォールバック)を失わせます。href を持つネイティブのアンカーに role="link" を追加しないでください。ロールはすでに提供されています。
Serious:意味のあるリンクテキスト
WCAG 2.4.4 では、リンクの目的をテキストのみ、またはテキストとプログラムで判定できる文脈から判断できることが求められています。どのような文脈もなくすべてのリンクを理解できることは、より厳しいレベル AAA の基準(2.4.9)ですが、依然として有用な目標です。
<a href="#installation">Installation instructions</a>
<a href="#wcag-criteria">Relevant WCAG success criteria</a>
<a href="#section">Click here</a>
<a href="#section">Read more</a>
「続きを読む」は、プログラムで判定できる文脈から目的が分かる場合、必ずしもレベル A の不適合になるわけではありません。ただし、より明確なテキストのほうが、ざっと確認しやすく、翻訳しやすく、音声による操作もしやすくなります。デザイン上、繰り返し表示されるテキストを維持する必要がある場合は、そのテキストを置き換えるのではなく、リンク内に非表示の文脈を追加してください。
<a href="#keyboard-testing">
Read more<span class="visually-hidden"> about keyboard testing</span>
</a>
有用な表示テキストを別の名前に置き換えるために aria-label を使用しないでください。アクセシブルネームには表示テキストを含める必要があります(音声入力と WCAG 2.5.3「ラベルを名前に含める」の要件に対応します)。アイコンのみのリンクでは、「リンク先」は冗長です。スクリーンリーダーはすでにロールを読み上げます。
<a href="#installation" aria-label="Installation section">
<svg aria-hidden="true" focusable="false"></svg>
</a>
同じ目的や遷移先を持つリンクには、一貫した名前を付けてください。同じテキストで異なる遷移先を持つリンクは、区別するのに十分な文脈を含めてください。
Serious:フラグメントターゲットは一意で、表示され、安定している必要があります
# の後のフラグメントは、ドキュメント内で一意の id を参照する必要があります。ID に ASCII の空白文字を含めてはいけません。
<nav aria-label="On this page"><a href="#installation">Installation</a></nav>
<h2 id="installation">Installation</h2>
hidden/display:none/visibility:hidden を持つ要素、閉じた開閉式コンテンツ内のコンテンツ、非アクティブなタブパネル、表示される見出しから離れた空のマーカー、またはハイドレーション中に削除される要素をターゲットにしないでください。折りたたまれたコンテンツやタブ付きコンテンツ内を指すディープリンクがある場合は、スクロールまたはフォーカス移動の前にコンテナを表示してください。
公開済みの ID は安定して保ってください: 遷移先が概念上同じであれば既存の ID を維持してください。ID を変更する必要がある場合は、エイリアスまたはリダイレクトの方針を用意してください。見出しのテキストや大文字・小文字が変わっただけで、すべての ID を再生成しないでください。スラッグを自動生成するシステムでは、重複する見出しを決定的に処理し、サーバー側のレンダリングとクライアント側のハイドレーションで同じ ID を生成する必要があります。
深刻:スキップリンク
最初にフォーカス可能なコントロール(または最初の少数のスキップリンクの一部)であり、フォーカス時に完全に表示される必要があります。
スキップリンクが恒久的に非表示(display:none、visibility:hidden)の場合は深刻です。WCAG 2.4.1 の目的が完全に損なわれます。
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>
<header></header>
<main id="main-content" class="skip-target" tabindex="-1">
<h1>Page title</h1>
</main>
</body>
.skip-link {
position: absolute;
z-index: 1000;
inset-block-start: 0;
inset-inline-start: 1rem;
padding: 0.75rem 1rem;
color: #ffffff;
background: #111827;
font-weight: 700;
transform: translateY(-150%);
}
.skip-link:focus {
transform: translateY(0);
outline: 3px solid #facc15;
outline-offset: 2px;
}
@media (forced-colors: active) {
.skip-link { color: LinkText; background: Canvas; border: 1px solid LinkText; }
.skip-link:focus { outline-color: Highlight; }
}
有効化後は、メインコンテンツにフォーカスが移動し、メインの見出しまたはコンテンツの先頭が表示され、次の Tab キーでメインコンテンツ内の最初の論理的なインタラクティブ要素に移動し、スキップしたナビゲーションが先にたどられないことを確認してください。tabindex="-1" を使うと、タブストップを追加せずにフォーカスできます。これは順次タブ移動の順序には含まれません。400% のズームと、翻訳文が長い場合にもテストしてください。フォーカスされたリンクが、ビューポート、Cookie バナー、固定ヘッダー、または overflow: hidden を持つ祖先要素によって切り取られてはいけません。
中程度:通常のページ内リンクのフォーカス管理
スクロールとフォーカスは関連していますが、別のものです。見た目上スクロールしただけでは、キーボードフォーカスやスクリーンリーダーの注視点が適切な場所に移動したことにはなりません。まずはネイティブ HTML を使い、クリックをキャンセルしないでください。
<a href="#testing">Testing</a>
<h2 id="testing">Testing</h2>
JavaScript でできるというだけの理由で、すべての見出しに tabindex="-1" を自動的に追加したり、フラグメントごとに .focus() を呼び出したりしないでください。ネイティブの動作では分かりやすい遷移が提供されない場合や、ナビゲーションが割り込まれた場合には、プログラムによるフォーカスが適切です。
function focusFragmentTarget(target) {
if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '-1');
target.focus({ preventScroll: true });
target.scrollIntoView({ block: 'start' });
}
正の tabindex を追加しないでください。非表示の要素にフォーカスを移動しないでください。通常はインタラクティブではない対象にフォーカスを移動する場合は、視覚的に分かる位置の手がかりを示してください。
.skip-target:focus, .programmatic-focus-target:focus {
outline: 3px solid currentColor;
outline-offset: 0.25rem;
}
中程度:スティッキーヘッダーと固定ヘッダー
フラグメントの対象やフォーカスされた要素がスティッキーヘッダーの下に隠れないように、CSS のスクロールオフセットを使用してください(WCAG 2.4.11「フォーカスが隠れないこと(最小)」もここに適用されます)。
:root { --anchor-offset: 5rem; }
html { scroll-padding-block-start: var(--anchor-offset); }
h2[id], h3[id], main[id], .anchor-target {
scroll-margin-block-start: var(--anchor-offset);
}
スクロールする要素自体に scroll-padding-block-start を設定してください。内部スクロール領域の場合、html のみに設定しても効果はありません。オフセットは、レスポンシブのブレークポイントやズームレベル、バナーの表示・非表示に応じたヘッダーの高さに合わせる必要があります。
深刻:スムーズスクロールでは prefers-reduced-motion を尊重する
最も堅牢なデフォルトは、即時のネイティブスクロールです。無条件の scroll-behavior: smooth は、前庭障害を引き起こす可能性があります。
メディアクエリによる制御なしにスムーズスクロールを全体に適用するのは深刻です。
/* Never unconditional */
/* Bad: html { scroll-behavior: smooth; } */
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
const allowMotion = window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
target.scrollIntoView({ behavior: allowMotion ? 'smooth' : 'auto', block: 'start' });
スムーズスクロールを、視差効果、アニメーションによる強調表示、その他の移動先を追いにくくする動きと組み合わせないでください。ネイティブのフラグメントナビゲーションで実現できる動作を再現するためだけに JavaScript を追加しないでください。
中程度:目次とスクロールスパイ
<nav aria-labelledby="contents-heading">
<h2 id="contents-heading">Contents</h2>
<ol>
<li><a href="#installation">Installation</a></li>
<li><a href="#configuration">Configuration</a></li>
</ol>
</nav>
リンクの文言を移動先の見出しに合わせ、見出しの階層を保ち、長い目次がユーザーが回避しなければならない別のブロックにならないようにしてください。
スクロールスパイでは、現在表示されているセクションに aria-current="location"("true" ではありません)を設定し、IntersectionObserver またはスクロールリスナーで更新してください。
<nav aria-labelledby="on-this-page-heading">
<ul>
<li><a href="#overview" aria-current="location">Overview</a></li>
<li><a href="#testing">Testing</a></li>
</ul>
</nav>
現在の状態も視覚的に示し、現在のリンクは一つだけにしてください。スクロールスパイの変更をライブリージョンで毎回通知したり、更新のたびに新しい履歴項目を追加したりしないでください。
中程度:見出しのパーマリンクとページ上部に戻るリンク
パーマリンクは見出しのフラグメントに移動します。アクセシブルネームが見出しテキストの一部にならないよう、見出しとは視覚的・構造的に分けてください。
<div class="heading-with-permalink">
<h2 id="installation">Installation</h2>
<a class="permalink" href="#installation">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">…</svg>
<span class="visually-hidden">Permalink to Installation</span>
</a>
</div>
パーマリンクには、セクションを識別できるアクセシブルネーム、視認できるフォーカスインジケーター、使いやすいターゲットサイズが必要です。また、アイコンでリンクを識別する場合は十分なコントラストを確保し、ポインターホバーなしでも利用できるようにしてください。有効化時に移動せず URL をコピーする場合は、「Installation へのリンクをコピー」という名前のボタンを使用し、コピー成功後にステータスメッセージを表示してください。
ページ上部に戻るリンクには、空の # ではなく、明示的なターゲットを使用してください。
<header id="page-top" class="programmatic-focus-target" tabindex="-1">…</header>
<a href="#page-top">Back to top</a>
ターゲットがスティッキーコンテンツの後ろに隠れないようにし、短いセクションごとにリンクを繰り返して煩雑にならないようにしてください。
重要:シングルページアプリケーションとルーター
アクセシブルなルーターは、ルートナビゲーションとフラグメントナビゲーションの両方に対応する必要があります。
- 使用可能な
hrefを持つ実際のアンカーをレンダリングします(例:/settings#notifications) — ルート/フラグメントナビゲーションにボタンを使ってはいけません - 修飾キーを使ったクリック、新しいタブで開く操作、コピー、スクリプトなしでの動作を維持します
- 遷移先のルートとターゲットがレンダリングされるまで待ちます。ターゲットに移動する前に、折りたたまれた親要素を展開します
- URL を更新して適切な履歴エントリを作成し、正しいコンテナーをスクロールし、ルート変更で必要な場合はフォーカスを移動します
- 初回読み込み時、
hashchange時、戻る/進むナビゲーション時のフラグメントを処理します - ユーザーが別の場所に移動した後、古い非同期ルートコンテンツがフォーカスを移動させないようにします
ルート全体の変更では、通常、ドキュメントタイトルとメイン見出しのフォーカス管理が必要です(skills/navigation/SKILL.md を参照)。同じページ内でフラグメントが変わった場合、同じルート変更の通知が自動的に必要になるわけではありません。
軽微:URL と履歴の安定性
- アクティベーション後も URL のフラグメントを維持します。これにより、ブックマーク、共有、再読み込み、戻る/進む操作に対応できます
- スクロール後にハッシュを削除しないでください。通常のアンカーナビゲーションで、戻る操作が機能しなくなる場合は
history.replaceState()を使わないでください - 意味のあるスラッグ ID(
#section-3ではなく#installation-guide)を使います。これは文書化された執筆規約であり、WCAG の要件ではありません(HTML では文字やハイフン以外の文字も使用できます) - ID にスペースを含めてはいけません。ページ内で ID は一意である必要があります
テスト
- キーボード(スキップリンク): ブラウザーのクロームから Tab キーを押します。スキップリンクが最初、または最初に近いフォーカス可能要素であることを確認します。フォーカスインジケーターとともに全体が表示されることを確認します。Enter キーでメインコンテンツにフォーカスが移動することを確認します。Tab キーを押すと、メインコンテンツ内の最初の論理的なコントロールに進むことを確認します
- キーボード(アンカーリンク): リンクに移動し、フォーカスインジケーターを確認します。Enter キーでアクティベートし、ターゲットが表示され、隠れていないことを確認します。Tab/Shift+Tab の移動が論理的であることを確認します。戻る/進む操作で履歴とフォーカスが維持されることを確認します
- スクリーンリーダー: リンク一覧で名前が有用か確認します。スキップリンクをアクティベートし、メインコンテンツが新しい位置になっていることを確認します。目次リンクをアクティベートし、遷移先が理解しやすいことを確認します。読み込み時/再読み込み時のフラグメント付き URL をテストします
- 視覚/ズーム/動き: 200%/400% のズーム、狭いビューポートをテストします。固定ヘッダー/バナーがターゲットやフォーカスを覆わないことを確認します。テキスト間隔の上書き、高コントラスト/強制カラーモード、動きを減らす設定でスクロールが即時に行われること、RTL コンテンツをテストします
- タッチ/音声入力: アクティベーション領域が十分であることを確認します。パーマリンクの操作にホバーが必要ないことを確認します。音声入力で、アクセシブルな名前が表示ラベルと一致することを確認します
自動チェックでは、アクセシブルな名前のないリンク、空またはダミーの href、解決できないフラグメント、重複 ID、ID 内の空白、正の値の tabindex、aria-hidden で隠されたフォーカス可能なコンテンツ、テキストによる代替がないアイコンのみのリンクを検出できます。ソースの Markdown だけでなく、ビルド済みの出力に対してリンクチェッカーを実行してください。自動化では、フォーカスの配置、固定ヘッダーとの重なり、動きの挙動、遷移先が理解しやすいかどうかは確認できません。
よくある失敗
| 失敗 | 修正方法 |
|---|---|
| フラグメントナビゲーションにボタンを使う | 実際の href を持つアンカーを使います |
JS アクションに href="#" を使う | ボタン、または実際のフラグメント遷移先を使います |
表示テキストを別の aria-label に置き換える | アクセシブルネームに表示テキストを含めます |
| 存在しない、または重複した ID にリンクする | 一致する一意のターゲットを作成します |
| 見出しテキストが変わるたびに ID を再生成する | 公開済みのディープリンクを維持します |
| すべての見出しに自動でフォーカスを移動する | ネイティブのナビゲーションを使い、必要な場合にのみフォーカス管理を追加します |
見出しに tabindex="0" を追加する | 意図的なプログラムによるフォーカスが必要な場合にのみ tabindex="-1" を使います |
| 固定ヘッダーの背後に隠れるコンテンツまでスクロールする | 適切なスクロールのパディング/マージンを設定し、レスポンシブ時のオフセットをテストします |
| 常にスムーズスクロールを使う | prefers-reduced-motion: no-preference を条件にします |
| スクロールスパイの更新ごとに履歴エントリーを追加する | 履歴を汚さずに現在の状態を更新します |
| 閉じている、または非アクティブなコンテンツ内にリンクする | ナビゲーションの前に、包含する開示パネル/パネルを表示します |
完了の定義チェックリスト
- すべてのリンクテキストが、文脈から切り離しても説明的で意味が通る
-
aria-labelに「リンク先」を使わない — ロールはすでに通知されます - ターゲット要素に一意の
id値があり、公開済みの ID は安定して維持されている - 非インタラクティブなターゲットでは、プログラムによるフォーカスが必要な場合にのみ
tabindex="-1"を指定している - スキップリンク:DOM の先頭(またはその近く)にあり、フォーカス時に表示され、
display:none/visibility:hiddenになっていない -
scroll-padding/scroll-marginにより、ターゲットとフォーカスの両方で固定ヘッダーとの重なりが解消されている - スムーズスクロールが
prefers-reduced-motion: no-preferenceで条件付けられている - アクティブな目次/スクロールスパイのリンクに
aria-current="location"を指定している("true"ではない) - 見出しのパーマリンクとページ先頭へ戻るリンクに、識別可能な名前と表示されるターゲットがある
- SPA ルーターが実際の
href、履歴、修飾キーを使ったクリック動作を維持している - ID がページ内で一意で、スペースではなくハイフンを使用している
- アンカーナビゲーションの後にフォーカストラップがない
主要な WCAG 基準
- 2.4.1 ブロックスキップ(A)— スキップリンクが壊れている、または非表示の場合は重大
- 2.4.4 リンクの目的(コンテキスト内)(A)— リンクテキストが曖昧な場合は重大
- 2.4.7 フォーカスの可視化(AA)
- 2.4.9 リンクの目的(リンクのみ)(AAA)— レベル A の最低要件を超える、優れた設計目標
- 2.4.11 フォーカスが隠されない(最低限)(AA、WCAG 2.2)— 固定ヘッダーがターゲットを隠す
- 2.5.3 ラベルを名前に含める(A)
- 3.2.4 一貫した識別(AA)
スムーズスクロールのアニメーションは、prefers-reduced-motion に沿ったベストプラクティスですが、
WCAG 2.2 AA では直接テストされません(2.3.3 インタラクションによるアニメーション
はレベル AAA です)。それでも、段階的拡張の基本としてこの条件は必須です。
参考資料
- ベストプラクティスの完全ガイド
- WCAG 2.2 解説 2.4.4 リンクの目的
- WCAG 2.2 解説 2.4.9 リンクの目的(リンクのみ)
- WCAG 2.2 解説 2.4.11 フォーカスが隠されない
- MDN:prefers-reduced-motion
標準の見通し: これらのルールは WCAG 2.2 AA を対象としています。アンカーリンクのパターンについて、 WCAG 3.0 での破壊的変更は予想されていません。 監視対象:https://www.w3.org/TR/wcag-3.0/