ページ内リンク(アンカーリンク)をクリックした際、固定ヘッダーの後ろに見出しが潜り込んで隠れてしまう現象は、Web制作の現場で非常によく遭遇するトラブルのひとつです。
現在の主要なブラウザでは、多くのケースでCSSのscroll-margin-topを指定するだけでシンプルに解決できます(主要なブラウザで広く利用可能な標準プロパティです)。
この記事では、見出しが隠れる原因から、scroll-margin-topを用いた最小構成の実装、実務での堅実なCSSの書き方、うまく効かないときの切り分けポイントまでを解説します。
アンカーリンクで見出しが固定ヘッダーに隠れる原因
アンカーリンク(<a href="#section1">など)をクリックすると、ブラウザはidが一致する要素を表示できる位置までスクロールします。
通常のページでは、リンク先要素が画面上部に来るような位置まで移動するため、position: fixed;で固定している場合や、position: sticky;でヘッダーが画面上部に張り付いている場合、リンク先要素とヘッダーが重なってしまいます。
【ブラウザの表示領域(ビューポート)】
┌────────────────────────────────────────┐
│ [ 固定ヘッダー (height: 80px) ] │ ← ここに重なる
├────────────────────────────────────────┤
│ (本来見せたい見出しが隠れてしまう領域) │
│ │
│ 本文テキスト... │
└────────────────────────────────────────┘
その結果、ブラウザが意図通りに要素の位置までスクロールしたとしても、上から被さっている固定ヘッダーの高さ分だけ見出しが隠れて見えなくなります。
scroll-margin-topを指定すればCSSだけで位置を調整できる
この挙動を調整するプロパティがscroll-margin-topです。
scroll-margin-topの最小構成
スクロールの着地先となる要素(idを持つ見出しなど)に対し、固定ヘッダーの高さを基準にscroll-margin-topを設定します。
/* 固定ヘッダーの高さが 80px の場合 */
h2[id] {
scroll-margin-top: 80px;
}
これにより、アンカーリンク先へ移動したときに、対象要素の上側へ80px分の余裕を持たせた位置で表示できます。
margin-topやpadding-topとの違い
以前よく使われていた「padding-topで余白を取り、同じ幅の負のmargin-topで相殺する」手法と比較すると、scroll-margin-topには以下のようなメリットがあります。
- 通常のレイアウトに影響を与えない:
marginやpaddingのように通常レイアウト上の余白を増減させません。また、paddingのように要素の背景領域やクリック可能な領域を広げることもありません。 - スクロール時の表示位置だけを調整できる:ページ内リンクや後述の
scrollIntoView()などで対象要素へ移動するときの位置を調整できます。
実務ではアンカーリンク先へまとめて指定すると使いやすい
個別の見出しタグに都度指定するだけでなく、サイト設計に合わせて破綻しにくいセレクタで指定しておくと管理が容易になります。
複数の見出しへまとめて指定する方法
アンカー先になり得る見出し要素やセクション、またはアンカー用の共通クラスに対して指定します。
/* idを持つ見出しやセクションに絞って指定 */
h2[id],
h3[id],
section[id] {
scroll-margin-top: 100px;
}
/* または専用のクラスを付与して管理する場合 */
.anchor-target {
scroll-margin-top: 100px;
}
※ [id]単体でサイト全体へ一括指定することも技術的には可能です。ただし、フォームの<input id="...">やJavaScriptの制御用要素など、意図しない要素が別の処理でスクロールされた場合にも影響します。規模の大きいサイトでは、上記のようにアンカーリンク先になる要素へセレクタを絞った方が安全です。
ヘッダーの高さ+余白を確保する方法
ヘッダーの下端にぴったり合わせるのではなく、少し余裕(20px〜30px程度)を持たせると視認性が高まります。
CSSカスタムプロパティ(CSS変数)を使うと、ヘッダーの高さ変更にも柔軟に対応できます。
:root {
--header-height: 80px;
--scroll-offset: 24px; /* 見出し上のゆとり */
}
h2[id],
h3[id],
section[id] {
scroll-margin-top: calc(var(--header-height) + var(--scroll-offset));
}
PCとスマホでヘッダーの高さが違う場合の書き方
レスポンシブデザインでPCとスマートフォンのヘッダー高さが変わる場合は、メディアクエリを使って変数を上書きします。
:root {
--header-height: 80px; /* PCのヘッダー高さ */
}
@media (max-width: 768px) {
:root {
--header-height: 60px; /* スマホのヘッダー高さ */
}
}
/* アンカー先への指定 */
h2[id],
h3[id],
section[id] {
scroll-margin-top: calc(var(--header-height) + 20px);
}
ルートの変数側を切り替えておけば、セレクタ側の指定を複数箇所に重複して書く必要がなくなります。
scroll-padding-topで調整する方法もある
着地先となる要素側ではなく、スクロールコンテナ側(スクロール領域を持つ親要素)に余白を設定するscroll-padding-topというプロパティもあります。
scroll-margin-topとの違い
scroll-margin-top:スクロールされる子要素(見出しなど)に指定する。「この要素の上側に余白を持たせてスクロールしてほしい」という指定。scroll-padding-top:スクロールコンテナ側に指定する。「このスクロール領域の上部には常にヘッダー分の進入禁止エリアがある」という指定。通常のページ全体のスクロールであればhtml、独自のスクロール領域を作っている場合はその要素が対象です。
ページ全体のスクロール位置をまとめて調整する場合
ページ全体でアンカーリンクなどのスクロール先に共通の上余白を持たせたい場合、html要素にscroll-padding-topを指定できます。
html {
scroll-padding-top: 100px;
}
※ scroll-behavior: smooth;と併用することも可能です。
特定のセクションだけ個別に余白を調整したい場合はscroll-margin-top、ページ全体で一括制御したい場合はhtmlへのscroll-padding-topを選択すると管理がシンプルになります。
scroll-margin-topを指定しても直らないときの確認点
scroll-margin-topを設定したのに位置がずれる場合、以下のポイントを確認してください。
1. idを付けた要素とCSSの指定先が合っているか
リンク先が<section id="sec1">なのに、CSSでh2 { scroll-margin-top: 80px; }と指定しているケースです。スクロールの着地基準はidが付与されている要素そのものになるため、idを持っている<section>側にscroll-margin-topを指定する必要があります。
2. JavaScriptでスクロール座標を直接指定していないか
window.scrollTo()やjQueryのanimate()などで最終的なスクロール座標をJavaScript側から直接指定している場合、scroll-margin-topを追加しただけでは期待する位置にならないことがあります。JavaScript側ですでにヘッダー分のオフセットを計算している場合は、CSS側の指定と二重に補正していないか確認してください。CSS側へ制御をまとめる場合は、JavaScript側のオフセット処理を不要にできることがあります。
3. ページ全体ではなく別の要素がスクロールしていないか
bodyやラッパー要素にoverflow-y: auto;がかかっており、ルート(html)ではなく内部の要素がスクロールコンテナになっている場合、htmlに指定したscroll-padding-topは効きません。スクロールが発生しているコンテナ要素に対して指定する必要があります。
4. 固定ヘッダーの高さが途中で変わっていないか
スクロール開始後にヘッダーが縮小するアニメーションや、アコーディオンメニューの開閉によってヘッダー高さが動的に可変する場合、固定値のscroll-margin-topだと停止位置にズレが生じることがあります。
scrollIntoView()を使っている場合も位置を調整できる
JavaScriptのelement.scrollIntoView()によるスクロールでも、対象要素に指定したscroll-margin-topを利用して表示位置を調整できます。
const target = document.querySelector('#section2');
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
JavaScript側でgetBoundingClientRect().top - headerHeightのような座標計算を独自に行わなくても、CSS側でscroll-margin-topを定義しておけば、標準のscrollIntoView()だけで固定ヘッダーを考慮した位置で表示させることができます。
まとめ
固定ヘッダーによってアンカーリンク先が隠れる問題は、CSSのスクロール関連プロパティを使うことで、JavaScriptで座標計算を行わずに解決できるケースが多くあります。
- 個別・要素ベースで制御したい場合:対象要素に
scroll-margin-topを指定(セレクタを絞って管理するのがおすすめ) - ページ全体で一括制御したい場合:スクロールコンテナ(
htmlなど)にscroll-padding-topを指定 - JS併用時:
scrollIntoView({ block: 'start' })でもCSSの余白設定が反映される
JavaScriptで独自にスクロール座標を計算する前に、まずはscroll-margin-topやscroll-padding-topで対応できないか確認すると、実装をシンプルに保ちやすくなります。