デザインから取り込んだコードでナビゲーションがはみ出したとき、リンクの入れ子を整理する必要がありました。aの中に別のaがあると、ソースに書いた親子関係がブラウザー上でも保たれるとは限りません。CSSが効く相手を調べるには、ソースと、HTMLを読み込んだあとのDOMを比べるのが役立ちます。
自分の作業では、Figmaから取得したコードをAstroの静的サイトへ取り込む中で、この問題に遭遇しました。修正後のナビゲーションは、各項目がそれぞれ1つのリンクになっています。今回はその経験をもとに、問題の仕組みを小さなHTMLへ切り出します。
デザインの階層とHTMLの親子関係がずれる
画面上では「サービス」という1つの項目でも、実装では外側のクリック領域と内側の文字が別の要素になっていることがあります。その両方をリンクとして出力すると、次のようなHTMLになります。
<nav id="menu" aria-label="メインナビゲーション">
<a class="item" href="#services">
<a href="#services">サービス</a>
</a>
<a class="item" href="#contact">相談する</a>
</nav>
外側に幅や余白、内側に表示する文字を持たせたつもりでも、これは適切なリンクの構造になっていません。HTML Standardでは、aの子孫に別のaを置くことは禁止されています。内側のhrefを消すだけでも、aの入れ子は解消しません。
一方、これを開いたからといって、必ず画面が真っ白になったり、JavaScriptの例外が出たりするわけではありません。ブラウザーがHTMLの解析規則に従って処理し、表示できるDOMを作るためです。「表示はされる」ということと、「書いた階層のまま表示される」ということは分けて考える必要があります。
Astroに限った話ではありませんが、テンプレート側のコードだけを見ていると、描画後にも同じ親子関係があると思ってしまいやすい場面でした。
まず、幅を持たせたリンクに文字が入っているか見る
同じ症状を調べるなら、開発者ツールのElementsでナビゲーションを選び、文字のあるリンクと、レイアウト用のクラスを持つリンクが同じ要素かを確認します。次のコードをConsoleで実行すると、直接の子要素を一覧にできます。
const menu = document.querySelector('#menu');
console.table([...menu.children].map((element) => ({
tag: element.tagName,
class: element.className,
text: element.textContent.trim(),
href: element.getAttribute('href'),
})));
上の最小例をHTML5パーサーで解析すると、ソース上で2項目に見えたナビゲーションが、次の3要素になります。
| 要素 | class | 表示文字 | href |
|---|---|---|---|
| a | item | 空文字 | #services |
| a | なし | サービス | #services |
| a | item | 相談する | #contact |
最初の.itemは残っていますが、その中に「サービス」の文字が入っていません。文字を持つ別のリンクが、隣に並んでいます。
ここで.itemにwidthやpaddingを足すと、空のリンクに対してもその指定が適用されます。navをFlexboxにしていれば、想定より多い子要素が並び、gapが入る箇所も変わります。CSSの値を直す前に、値を適用している相手が想定どおりかを見る理由はここにあります。
ブラウザーで要素数や親子関係が違っていた場合は、NetworkのDocumentレスポンス、またはビルド後のHTMLと比較します。テンプレート、出力されたHTML、ElementsのDOMを順に見ると、どこで構造が変わったかを切り分けられます。
JavaScriptでもDOMは変わるので、違いがあればすべてHTMLパーサーのせいというわけではありません。この最小例にはJavaScriptを入れず、HTMLを読み込むだけでも構造が変わる条件を確認します。
同じリンク先なら、aを1つにする
外側の領域も内側の文字も同じ場所へ移動するなら、リンクを1つにして、文字や装飾をspanへまとめます。
<nav id="menu" aria-label="メインナビゲーション">
<a class="item" href="#services">
<span>サービス</span>
</a>
<a class="item" href="#contact">相談する</a>
</nav>
これで、見た目の領域、リンク先、表示文字が同じ要素にまとまります。文字だけに色や余白を付けたい場合は、内側のspanへクラスを付けられます。
今回の独立した再検証では、Pythonのhtml5lib 1.1で、掲載した修正前・修正後のHTMLを解析しました。修正前は直接の子要素が3つになり、空の.itemとクラスのない「サービス」のリンクが分かれました。修正後は子要素が2つになり、両方の.itemに表示文字と意図したリンク先が入りました。修正前には解析エラーが記録され、修正後の例では記録されませんでした。
これはHTML解析の確認です。今回の再検証では実ブラウザーの描画やクリック操作までは実行できていないため、以下のCSSでの表示幅・操作は、公開前にブラウザーで確認する手順として残しています。
もともとの作業でも、修正後のPC用・スマートフォン用のナビゲーションは、それぞれ4つの独立したリンクになっていることをソースで確認しました。ただし、リンクを整理することと、狭い画面で必要なレイアウトを設計することは別です。正しいHTMLになっていても、固定幅や長い文字列によるはみ出しは残り得ます。
リンク先が違うものを1つのクリック領域へ重ねたい場合は、単純に内側をspanへ変えるだけでは要件を満たしません。その場合は、外側をdivなどの非対話要素にし、リンクを別々に配置します。ボタンなどをリンクの内側へ入れて解決する方法も避けます。
はみ出しとリンク先を別々に確認する
構造の変化を見やすくするため、再現用には次のCSSを使いました。各項目の幅は140px、間隔は16pxです。この値は元サイトの設計値ではなく、空のリンクが増えた影響を確認するために置いています。
body { margin: 12px; }
#menu { display: flex; gap: 16px; }
.item {
box-sizing: border-box;
flex: 0 0 140px;
padding: 12px;
}
このCSSと、修正前・修正後のHTMLをそれぞれ保存して比較します。HTML文書には<!doctype html>、UTF-8指定、次のviewport指定を付けます。モバイルでの確認では、viewport指定の有無も条件に含めます。
<meta name="viewport" content="width=device-width, initial-scale=1">
横のはみ出しは、開発者ツールの表示幅を変えて、次の式で確認できます。
document.documentElement.scrollWidth >
document.documentElement.clientWidth
trueなら、文書全体に横方向のはみ出しがあります。今回のような構造の問題を追う入口には使えますが、この結果だけでは、どの要素が原因かまでは分かりません。また、overflow-x: hiddenで隠れている場合などには、見た目の欠けも別に確認します。
修正後は、要素数だけでなく「サービス」をクリックするとサービスへ、「相談する」をクリックすると問い合わせへ移動することも確認します。PC用とスマートフォン用で別のHTMLを持つ構成なら、片方だけを修正していないか、移動先のIDが実際に存在するかも見ます。
再発防止では、DOMだけの検査に頼らない
再発を防ぐために、描画後のDOMでa aを検索したくなります。ただ、この例ではブラウザーがリンクを兄弟要素に分けています。document.querySelectorAll('a a')が0件でも、出力したHTMLに入れ子がなかった証明にはなりません。
出力HTMLの適合性検査と、ブラウザーでの振る舞いの確認を組み合わせます。前者は生成段階の不正な構造を見つけるため、後者は表示文字、リンク先、空のクリック領域、横のはみ出しを確認するためです。デザイン由来のコードを追加するときは、特にリンク・ボタン・フォームの境界を見直すようにします。
HTML Standardの解析規則には、すでに処理中のaがあるときに次のa開始タグを受け取った場合の処理が定められています。ただし、不正なHTMLがどのようなDOMになるかは、周囲の要素にも左右されます。「入れ子はいつもこの3要素になる」と一般化せず、自分の出力HTMLで比較するのが確実です。
まとめ
今回の学びは、見た目の問題でもHTMLの構造を確かめる必要がある、ということでした。デザインの階層をそのまま要素へ移すと、1つの項目にリンクが重なることがあります。
ソースで親子に見えても、ブラウザー上では違う要素へCSSが適用されているかもしれません。出力HTMLとDOMを比べ、リンクを整理したあとに、画面幅と操作を確認する。この順に見ると、構造の問題と、残ったレイアウトの問題を分けて調べられます。