a11y/id-duplication · id の重複
ルート内の id はすべて一意にしましょう。
重大度: warning · カテゴリ: a11y
チェック内容
ルートを構成するレイアウトチェーン(+layout.svelte から +page.svelte まで)と、そこから解決したローカルコンポーネントを合わせて、同じリテラルな id の値が2回以上現れる場合を検出します。分岐を考慮して判定し、{#if}/{#await} ブロックでは出現数が最も多いアーム(同数ならドキュメント順で先のアーム)だけを数えるため、1 つの {#if} のアーム同士が合算されることはありません。ただし別々の {#if} ブロック同士は独立です。{#if a}…{/if}{#if !a}…{/if} は両方描画されるものとして数えます。条件式を評価するわけではないためです。
id はファイルをまたいで検出します。+layout.svelte の id="search" と +page.svelte(やインポートした $lib コンポーネント)の別の id="search" は、どちらのファイル単体を見ても問題なく見えますが、1つの重複です。ファイル単位のマークアップ linter は、レイアウトとページを合成して初めて存在するこの重複を検出できません。
検出しないもの:
- すべての id が一意なルート。
{#each}の中にある id。ループは0〜N回描画されうるため、重複するかどうかは静的には判定できません。実際に重複が発生するケース(N≥2回の描画)は、レンダリングモードが実行時に検出します。これは仕様上の意図的な違いです。- 動的な id(
id={expr})。実行時の値が静的にはわからないため対象外です。
なぜ重要か
id が重複すると、id の存在意義そのものが壊れます。<label for>・aria-labelledby・aria-describedby はその id を持つ最初の要素に解決され、ページ内フラグメントナビゲーション(#anchor)も最初に一致した要素へジャンプします。レイアウトとページが同じ id を導入してしまった場合によくあるように、作者の意図が2つ目の要素にあったとしても、支援技術やフラグメントリンクは見た目には何の異常もないまま、意図しない要素を指してしまいます。
修正方法
重複している id のどちらか一方を、ルート内で一意になるように名前を変えます。
<nav id="site-search">...</nav>
<section id="page-search">...</section>
<!-- id="site-search" だったものをレイアウトとの重複を避けて変更 -->
モードによる違い
両モードとも id を収集しますが、収集元が異なるため結果が食い違うことがあります。
- ソース解析(CLI、ダッシュボードの静的ベースライン)は、ルートのレイアウトチェーンと、そこから解決したローカルコンポーネントを、分岐を考慮した判定で合成します。
{#if}/{#await}ブロックでは出現数が最も多いアームだけを数えるため、実際にはレンダリングされないアームを選んでしまうことがあります。解決できないコンポーネント(node_modulesや動的に選ばれるコンポーネント)が持つ id は見えず、{#each}の中身は重複するかどうかが静的には判定できないため対象外です。 - 両モードとも、
src/app.htmlの id と衝突するページ側の id を報告します。ソース解析はシェル側の出現を「最初の、検出対象外の代表」として先頭に挿入するため、検出はルート側の出現に付き、メッセージにはシェルの名前が入ります(— also defined by the src/app.html shell (line N))。 - レンダリング解析(Vite プラグインのビルド、ダッシュボードで訪問したルート)はレンダリング済みの HTML を読みます。そのため、実際にレンダリングされた id だけを認識します。
{#each}が生成した id もすべてここに含まれます。ループ由来の実際の重複は、このモードでのみ表面化します。ソースファイルを持たないため、検出結果の位置は特定のファイル・行ではなくルート自体に紐づきます。同じ不具合でも、永続化される検出キーはモードによって異なります。
両者が食い違う場合は、レンダリング結果を信頼してください。ブラウザに配信される内容を反映しているのはそちらです。
無効化
検出が指す行の直上にインラインの svelte-vitals-disable-next-line コメントを置けば抑制できます(ソース解析のみ。ビルドの検出はプリレンダリングされた HTML を指すため、直上に置くべきソース行がありません)。その行が合成されたコンポーネント内にある場合、1 つのディレクティブがそれを合成するすべてのルートで効きます。ルート単位で抑制したい場合は suppressions ファイル(npx svelte-vitals --update-suppressions)を使ってください。overrides でルートやパス単位に絞るか、ルールごと無効化するには、次のように設定します。
export default {
rules: {
'a11y/id-duplication': 'off'
}
};