a11y/no-missing-id-ref · id参照切れ
id参照は、合成後のルートのどこかに存在する id を指している必要があります。
重大度: warning · カテゴリ: a11y
チェック内容
literal な id 参照属性、つまり HTML の for・list・headers・form・popovertarget・commandfor と ARIA のすべての id 参照プロパティ(aria-labelledby・aria-describedby・aria-controls・aria-activedescendant・aria-owns・aria-details・aria-errormessage・aria-flowto)、または同一ページ内を指す href="#…" について、参照先の id がルートを構成するレイアウトチェーン(+layout.svelte から +page.svelte まで)・そこから解決したローカルコンポーネント・src/app.html シェルのどこにも存在しない場合に検出します。
href="#top" は対象外です。この断片には id="top" を持つ要素が存在しなくても、ブラウザがドキュメント先頭までスクロールするため、参照切れにはなりません。テキストフラグメントのディレクティブは照合前に取り除きます。:~: 以降はテキストを探すためのユーザーエージェント向けの指示で要素を指さないため href="#:~:text=…" は何も参照しませんが、href="#section:~:text=…" は href="#section" と同じく id="section" に対して照合されます。
このルールは、他のルート単位 a11y ルールに比べて実行されるルート数がかなり少なくなります。 他のルート単位 a11y ルールはいずれも「これが2つ以上存在する」という主張であり、解決できないコンポーネントが何を持っていようとその主張は揺らぎません(すべてのルートで実行され、解決できないコンポーネントは偽陰性のコストで済みます)。一方このルールは「どこにもこの id を定義する要素が存在しない」という逆の主張であり、解決できないコンポーネントこそがまさにその id を持っている可能性があります。そのため閉世界を必要とし、ルートの構成全体が解決済みであると確認できた場合にのみ実行します。条件は、推移的閉包に含まれるすべてのコンポーネントが実際に解決済みで(node_modules/ライブラリからのインポート・.svelte ではなく index.ts に解決される barrel インポート・動的に選ばれるコンポーネントのいずれも無く)、深さ制限による打ち切り・{@html}・スプレッド属性が存在せず、合成対象のどのファイルにも動的な id が無いことです。いずれか1つでも満たさないルートは丸ごとスキップされ、そのルートについては何も出力しません(偽の「合格」にはしません)。
実際には、ルートの構成のどこか一箇所にでもライブラリコンポーネント(UIキットの <Button>、ルーティングヘルパーの <Link>、node_modules 配下の何か)があるだけで、閉世界はどこにも成立しなくなり、そのルートではこのルールは実行されません。ルートレイアウトにそうしたコンポーネントを1つでも使っている典型的なアプリでは、どのルートでもこのルールは実行されないことになります。これは不具合ではなく、受け入れた上での判断です。ここで誤検出を出すと、実際には解析が見えないコンポーネントの中に存在する id を、存在しないものとして探し回らせてしまいます。ルートを丸ごとスキップする方が、推測するよりましです。
スキップはもう静かには起きません。解析対象のルートが1つでもスキップされると、CLI はスキップ数/解析数の比率と原因、これが失敗ではないこと、そしてこのページへのリンクを示す1行を stderr に出力し、JSON レポートのトップレベルには skipped["a11y/no-missing-id-ref"] 配列が加わります。各エントリはスキップされたルート1件につき1つで、そのルートの literal な id 参照数(refs)と、閉世界の条件を満たせなかった原因それぞれ(component(コンポーネント名付き)・spread・html({@html})・dynamic-id)を、その条件に最初に該当したファイルと行とともに含みます。これにより、ルールが一度も実行されなかったレポートと、どこでも合格したレポートとを区別できるようになり、各エントリは実行を妨げている原因とその最初の位置を示します。
オプトインの姉妹ルールである a11y/unverified-id-ref を使えば、これらのスキップされたルートをオープンワールドとしてチェックし、一致しない参照を「欠落」ではなく「未検証」として報告できます。
なぜ重要か
id とその参照先は別々のファイルにあることが珍しくありません。フォームコンポーネント内の <label for="email">、いくつも離れたコンポーネントにある id="email" の <input>、実際のページが合成されて初めて意味を持つアンカーリンクなど。ファイル単位のマークアップ linter はこれをまったく検出できません。この不具合は、ルートが複数ファイルにまたがって合成されて初めて存在するからです。支援技術は for/aria-labelledby/aria-describedby/aria-controls/aria-activedescendant を最終的な DOM 上で id 検索して解決します。参照先が存在しなければ、この関連付けは何の警告もなく失われます。ラベルは無関係なテキストとして読み上げられ、aria-describedby は追加の説明を何も読み上げず、フラグメントリンクはどこにもスクロールしません。
修正方法
参照先の要素に id を追加するか、typo している参照を直します。
<label for="email">Email</label>
<input id="email" type="email" />
モードによる違い
参照チェックは両モードで実行されますが、収集元が異なるため結果が食い違うことがあります。
- ソース解析(CLI、ダッシュボードの静的ベースライン)は、ルートのレイアウトチェーンと、そこから解決したローカルコンポーネントを合成し、上記の閉世界条件を要求します。合成対象のどこかに解決できないコンポーネント・
{@html}・スプレッド属性・動的な id が1つでもあれば、ルート全体をスキップします。 - レンダリング解析(Vite プラグインのビルド、ダッシュボードで訪問したルート)はレンダリング済みの HTML を読みます。これは構造上すでに閉世界であるため、ブラウザに配信されるすべての id と参照が見えており、このモードには対応するスキップ条件がなく、ソース解析では実行できないルートもチェックできます。ソースファイルを持たないため、検出結果の位置は特定のファイル・行ではなくルート自体に紐づきます。同じ不具合でも、永続化される検出キーはモードによって異なります。
両者が食い違う場合は、レンダリング結果を信頼してください。ブラウザに配信される内容を反映しているのはそちらです。
無効化
検出が指す行の直上にインラインの svelte-vitals-disable-next-line コメントを置けば抑制できます(ソース解析のみ。ビルドの検出はプリレンダリングされた HTML を指すため、直上に置くべきソース行がありません)。その行が合成されたコンポーネント内にある場合、1 つのディレクティブがそれを合成するすべてのルートで効きます。ルート単位で抑制したい場合は suppressions ファイル(npx svelte-vitals --update-suppressions)を使ってください。overrides でルートやパス単位に絞るか、ルールごと無効化するには、次のように設定します。
export default {
rules: {
'a11y/no-missing-id-ref': 'off'
}
};