ページ内リンクのターゲットが固定ヘッダーに重なるのを CSS で対処する
ページ内にアンカーリンクを貼ってジャンプすると、リンク先のターゲット (#~) がブラウザのビューポートの最上部になるところまでスクロールされます。
その時、サイトのヘッダーを最上部に固定しているとその高さ分見えなくなってしまうというありがちな問題に CSS だけで対処する方法です。
コード例
以下のようなコードのとき、<a href=#hoge>hoge</a>
をクリックしてジャンプすると、 <h3 id="hoge">hoge<h3>
は‘<header>
の下に隠れて見えなくなってしまいます。
style.css
header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
}
index.html
<header>
Site Title
</header>
...
<a href="#hoge">hoge</a>
...
<h3 id="hoge">hoge</h3>
対処法
ターゲットになる要素の疑似クラスと疑似要素を使って余白を提供するのが (個人的には) 一番良さそうです。
:target
疑似クラス
通常アンカーリンクで遷移すると URL が書き換えられます。
ページ内リンクをクリックしたときには、https://www.***.com/index.html#hoge
のように URL の末尾にジャンプ先の id
(hoge
)がつき、その id
の要素は :target
疑似クラスで取得できます。
::before
疑似要素
今回は :target
疑似クラスを持つ要素の ::before
疑似要素を使って余白を提供したいと思います。
個人的な好みですが、固定ヘッダー分ターゲット要素の上部に余白を取りたいので ::before
疑似要素が合っているかなと思いました。
コード
:target::before {
content: "";
display: block;
height: 64px; /* ずらしたい高さ */
margin-top: -64px; /* heightに対するネガティブマージン */
visibility: hidden,
}
Web アプリなどで history が書き換わらない場合
URL に #~
が付かないと :target
疑似クラスが使えないので上の方法が使えません。
(場合によりますが)これまで取り組んだ中では属性セレクターで id
がある要素を絞り込むのが手軽でした。
h3[id]::before {
content: "";
display: block;
height: 64px;
margin-top: -64px;
visibility: hidden,
}
同様の問題でよく見るもの
CSS で対処する別のパターン
元の要素に <header>
の高さ (ずらしたい高さ) 分の padding
とそれを打ち消すためのネガティブな margin
を指定する方法。
h3#hoge {
padding-top: 64px;
margin-top: -64px;
}
この方法でももちろん固定ヘッダーに重なることなくページ内リンクでジャンプできます。
ただし、この方法を取ってしまうと border
などをつけるときに padding
が影響してしまうので、疑似要素を使ったほうがいいと思いました。
JavaScript
改めて検索すると JavaScript でヘッダー分ずらすなどの方法が結構出てきます。
しかし scroll
はグローバルイベントですし、addEventListener
してしまうと passive: true
で無い場合にはスクロールをブロックしてしまうのでやりたくありませんでした。
また、Web アプリなどでスクロールにイベントリスナーを登録するといろいろ副作用があるので極力使わないようにしたほうが良いと思いました。
(例)
-
Angular の場合
グローバルイベントが
NgZone
に監視されているので、スクロールの度chengeDetection
に引っかかりngFor
などが再描画される。zone-flags
で監視対象外にするか、対象コンポーネントのchangeDetection
をChangeDetectionStrategy.onPush
にして手動で更新するなどの対応が必要となる。 -
SSR/SSG を行う場合
サーバーサイドには Window オブジェクトが無いため、サーバーで実行されるコードに scroll のリスナーが含まれているとエラーとなる。
例えば React なら
componentDidMount
やuseEffect()
(React.FC の場合) などサーバーで実行されない部分に記述するなどの対応が必要。どうしてもサーバーサイドで通過する場合はには
if (typeof window !== 'undefined')
で無視するなど。
あとがき
疑似クラスはめちゃくちゃ便利。