固定ヘッダーのあるサイトでアンカーリンクを使用すると、ジャンプ先がヘッダーの下に隠れてしまう問題があります。この問題を解決する方法として scroll-padding-top が広く知られていますが、実は リロード時にページがズレる という問題があることをご存知でしょうか。

この記事では、scroll-padding-top の問題点と、CSS変数$\small{\mathrm{と}}$JavaScriptを組み合わせた代替手法を紹介します。


scroll-padding-topとは

scroll-padding-top は、スクロールスナップやアンカーリンクのジャンプ位置を調整するCSSプロパティです。

html {
	scroll-padding-top: 80px; /* ヘッダーの高さ分 */
	scroll-behavior: smooth;  /* スムーススクロール */
}

たったこれだけで、アンカーリンクのジャンプ先がヘッダーの高さ分だけ上にオフセットされます。主要ブラウザでサポートされており、非常にシンプルな解決策として多くのサイトで採用されています。


scroll-padding-topの問題点

リロード時にページがズレる

scroll-padding-top を設定したサイトで、フロントページ(トップページ)をリロードすると、本来ページの先頭から表示されるべきところが、scroll-padding-top の値だけ下にズレて表示されることがあります。

例えば scroll-padding-top: 150px; と設定している場合、リロードするとページが150px分下にスクロールした状態で表示されてしまいます。

なぜリロード時にズレるのか

この現象は、ブラウザの スクロール位置復元機能(Scroll Restoration) と scroll-padding-top が競合することで発生します。

  1. ブラウザのスクロール復元機能 : ブラウザは通常、ページのリロード時に前回のスクロール位置を記憶して復元しようとします
  1. scroll-padding-topの適用タイミング : CSSで定義されたパディングが計算に反映される前にスクロールが完了してしまうことがあります
  1. 適用対象の問題 : scroll-padding-top は通常、html要素やbody要素、あるいはスクロールコンテナ自体に適用する必要があります。これが正しく適用されていないと、初回のクリック時は動作しても、リロードなどのシステム的なスクロール時には無視されることがあります
  1. 動的なコンテンツの読み込み : 画像やWebフォント、あるいはJavaScriptによる要素の生成など、ページの高さがリロード後に動的に変わる場合、ブラウザが位置を特定した時点と最終的なレイアウトが完成した時点で位置に差異が生じます

リロード時の挙動はブラウザの仕様(Chrome, Firefox, Safari等)によって微妙に優先順位が異なるため、すべての環境で完璧に動作させるには、前述のようなJavaScriptによる補正が必要になるケースが依然として多いのが現状です。

JavaScriptによるスクロール制御との競合

scroll-padding-top や scroll-margin-top は、 JavaScriptでスクロール動作を制御している場合、無視されることがあります 。

具体的には以下のようなケースで問題が発生します:

  • jQueryプラグイン : WordPressの目次プラグイン(Easy Table of Contents など)がsmooth-scrollスクリプトを使用している場合
  • Next.js : App Routerの Link コンポーネントでアンカーリンクを使用した場合(v13.4.3で修正済み)
  • React Router : 同様にJavaScriptでルーティングを制御している場合
  • カスタムスムーススクロール : 独自のJavaScriptでスクロールアニメーションを実装している場合

これは、JavaScriptが window.scrollTo() や element.scrollIntoView() などのAPIを使用してスクロール位置を直接制御する場合、CSSの scroll-padding プロパティが考慮されないことが原因です。

つまり、 CSSだけで完結するネイティブなアンカーリンク では scroll-padding-top は正しく動作しますが、JavaScriptが介在すると効かなくなる可能性があります。

その他の制約

  • ヘッダーの高さがPC/SPで異なる場合、メディアクエリでの切り替えが必要
  • 個別のアンカーポイントごとに異なるオフセット値を設定できない
  • 可変高さのヘッダー(スクロールで縮小するなど)への対応が難しい

解決策:CSS変数とJavaScriptを使った方法

scroll-padding-top の代わりに、従来の margin-top と padding-top を使った手法をCSS変数とJavaScriptで柔軟に実装します。

メリット

  1. リロード時のズレが発生しない : ブラウザのスクロール復元機能と競合しない
  2. 柔軟性 : 個別の要素ごとに異なるオフセット値を設定可能
  3. メンテナンス性 : ヘッダーの高さが変更された場合、CSS変数を更新するだけで全体に反映
  4. レスポンシブ対応 : メディアクエリでCSS変数を切り替えるだけで対応
  5. デザイナーフレンドリー : HTMLの属性やインラインスタイルで簡単に調整可能

コードと解説

1. CSS

:root {
	--header-height: 4pc;
	--anchor: var(--header-height);
}

@media print, screen and (min-width: 744px) {
	:root {
		--header-height: 5.5pc;
		--anchor: var(--header-height);
	}
}

/* アンカーリンク位置調整(フォールバック用) */
[anchor] {
	margin-top: calc(var(--anchor) * -1);
	padding-top: var(--anchor);
}

:root セレクタでグローバルなCSS変数を定義しています。--header-height はヘッダーの高さ、--anchor はアンカーリンクの調整値です。メディアクエリで画面サイズに応じて値を変更できます。

2. HTML

<!-- デフォルトの調整を使用(--anchor変数が適用される) -->
<section id="about" anchor>
	<h2>About</h2>
	...
</section>

<!-- カスタム値で調整 -->
<section id="service" anchor="120px">
	<h2>Service</h2>
	...
</section>

<!-- 単位省略可(pxとして扱われる) -->
<section id="contact" anchor="100">
	<h2>Contact</h2>
	...
</section>

<!-- CSS変数を直接指定 -->
<section id="news" anchor="var(--header-height)">
	<h2>News</h2>
	...
</section>

<!-- IDなしでもOK(自動生成される) -->
<hr anchor="5pc">

anchor 属性を追加するだけで、アンカーリンクの位置調整が適用されます。値を指定すれば個別のオフセットも可能です。[^1]

3. JavaScript

(function () {
	//_instyle-anchor.js
	//[anchor] 属性を持つすべての要素を取得
	const elements = document.querySelectorAll('[anchor]');

	//スタイル要素を作成(一度だけ作成)
	const STYLE = document.createElement('style');
	document.head.appendChild(STYLE);

	elements.forEach((el) => {
		//anchor 属性から値を取得
		const anchorValue = el.getAttribute('anchor');

		//IDを取得、なければランダムIDを生成
		let id = el.id || 'anchor-' + Math.random().toString(36).substr(2, 9);
		el.id = id;

		let anchorStyle = '';

		if (anchorValue) {
			//値と単位を分離する正規表現('r'も含める)
			const match = anchorValue.match(/^(\d*\.?\d+)(px|pc|r|rem|vw|vh|em)?$/);

			if (match) {
				const value = parseFloat(match[1]);
				let unit = match[2] || 'px'; //単位が指定されていない場合はpxとする

				//'r' を 'rem' に変換
				if (unit === 'r') {
					unit = 'rem';
				}

				//CSSルールを生成(数値+単位の場合)
				anchorStyle = `
				#${id} {
					margin-top: -${value}${unit};
					padding-top: ${value}${unit};
				}
				`;
			} else {
				//数値・単位でない場合(CSS変数など)はそのまま使用
				anchorStyle = `
				#${id} {
					margin-top: calc(-1 * (${anchorValue}));
					padding-top: ${anchorValue};
				}
				`;
			}
		} else {
			//属性値がない場合はCSS変数をフォールバック
			anchorStyle = `
			#${id} {
				margin-top: calc(-1 * var(--anchor));
				padding-top: var(--anchor);
			}
			`;
		}

		//スタイルに追加(エラーハンドリング付き)
		try {
			STYLE.sheet.insertRule(anchorStyle, STYLE.sheet.cssRules.length);
		} catch (e) {
			console.error(`Failed to insert CSS rule for element with id "${id}": ${e.message}`);
		}
	});
})();

JavaScriptの動作

  1. anchor 属性を持つすべての要素を選択
  2. 新しい <style> 要素を作成
  3. 各要素に対して:
    • anchor 属性の値を取得
    • IDがなければランダムIDを自動生成
    • 値の形式に応じてCSSルールを生成
    • 生成したルールをスタイルシートに挿入

対応する値の形式

指定方法 例 結果
数値+単位 anchor="120px" margin-top: -120px; padding-top: 120px;
数値のみ anchor="100" margin-top: -100px; padding-top: 100px;
rem省略形 anchor="5r" margin-top: -5rem; padding-top: 5rem;
CSS変数 anchor="var(--header-height)" CSS変数がそのまま適用
値なし anchor var(--anchor) が適用

まとめ

scroll-padding-top はシンプルで便利なプロパティですが、Scroll Restoration(ブラウザのスクロール位置復元機能)以外にも「フォーカス移動時のスクロール」「Safari の rem 解釈」など、複数の落とし穴があります。

CSS変数とJavaScriptを組み合わせた手法なら:

  • リロード時のズレが発生しない
  • 個別のアンカーポイントごとに異なるオフセット値を設定できる
  • レスポンシブ対応も容易
  • HTMLの属性で直感的に調整できる

固定ヘッダーを使用するレスポンシブデザインのウェブサイトで特に有用な手法です。



参考

[^1]: 厳密には、data-anchor の方がいいかもしれません。

固定ヘッダーのあるサイトでアンカーリンクを使用すると、ジャンプ先がヘッダーの下に隠れてしまう問題があります。この問題を解決する方法として scroll-padding-top が広く知られていますが、実は リロード時にページがズレる という問題があることをご存知でしょうか。

この記事では、scroll-padding-top の問題点と、CSS変数$\small{\mathrm{と}}$JavaScriptを組み合わせた代替手法を紹介します。


scroll-padding-topとは

scroll-padding-top は、スクロールスナップやアンカーリンクのジャンプ位置を調整するCSSプロパティです。

html {
	scroll-padding-top: 80px; /* ヘッダーの高さ分 */
	scroll-behavior: smooth;  /* スムーススクロール */
}

たったこれだけで、アンカーリンクのジャンプ先がヘッダーの高さ分だけ上にオフセットされます。主要ブラウザでサポートされており、非常にシンプルな解決策として多くのサイトで採用されています。


scroll-padding-topの問題点

リロード時にページがズレる

scroll-padding-top を設定したサイトで、フロントページ(トップページ)をリロードすると、本来ページの先頭から表示されるべきところが、scroll-padding-top の値だけ下にズレて表示されることがあります。

例えば scroll-padding-top: 150px; と設定している場合、リロードするとページが150px分下にスクロールした状態で表示されてしまいます。

なぜリロード時にズレるのか

この現象は、ブラウザの スクロール位置復元機能(Scroll Restoration) と scroll-padding-top が競合することで発生します。

  1. ブラウザのスクロール復元機能 : ブラウザは通常、ページのリロード時に前回のスクロール位置を記憶して復元しようとします
  1. scroll-padding-topの適用タイミング : CSSで定義されたパディングが計算に反映される前にスクロールが完了してしまうことがあります
  1. 適用対象の問題 : scroll-padding-top は通常、html要素やbody要素、あるいはスクロールコンテナ自体に適用する必要があります。これが正しく適用されていないと、初回のクリック時は動作しても、リロードなどのシステム的なスクロール時には無視されることがあります
  1. 動的なコンテンツの読み込み : 画像やWebフォント、あるいはJavaScriptによる要素の生成など、ページの高さがリロード後に動的に変わる場合、ブラウザが位置を特定した時点と最終的なレイアウトが完成した時点で位置に差異が生じます

リロード時の挙動はブラウザの仕様(Chrome, Firefox, Safari等)によって微妙に優先順位が異なるため、すべての環境で完璧に動作させるには、前述のようなJavaScriptによる補正が必要になるケースが依然として多いのが現状です。

JavaScriptによるスクロール制御との競合

scroll-padding-top や scroll-margin-top は、 JavaScriptでスクロール動作を制御している場合、無視されることがあります 。

具体的には以下のようなケースで問題が発生します:

  • jQueryプラグイン : WordPressの目次プラグイン(Easy Table of Contents など)がsmooth-scrollスクリプトを使用している場合
  • Next.js : App Routerの Link コンポーネントでアンカーリンクを使用した場合(v13.4.3で修正済み)
  • React Router : 同様にJavaScriptでルーティングを制御している場合
  • カスタムスムーススクロール : 独自のJavaScriptでスクロールアニメーションを実装している場合

これは、JavaScriptが window.scrollTo() や element.scrollIntoView() などのAPIを使用してスクロール位置を直接制御する場合、CSSの scroll-padding プロパティが考慮されないことが原因です。

つまり、 CSSだけで完結するネイティブなアンカーリンク では scroll-padding-top は正しく動作しますが、JavaScriptが介在すると効かなくなる可能性があります。

その他の制約

  • ヘッダーの高さがPC/SPで異なる場合、メディアクエリでの切り替えが必要
  • 個別のアンカーポイントごとに異なるオフセット値を設定できない
  • 可変高さのヘッダー(スクロールで縮小するなど)への対応が難しい

解決策:CSS変数とJavaScriptを使った方法

scroll-padding-top の代わりに、従来の margin-top と padding-top を使った手法をCSS変数とJavaScriptで柔軟に実装します。

メリット

  1. リロード時のズレが発生しない : ブラウザのスクロール復元機能と競合しない
  2. 柔軟性 : 個別の要素ごとに異なるオフセット値を設定可能
  3. メンテナンス性 : ヘッダーの高さが変更された場合、CSS変数を更新するだけで全体に反映
  4. レスポンシブ対応 : メディアクエリでCSS変数を切り替えるだけで対応
  5. デザイナーフレンドリー : HTMLの属性やインラインスタイルで簡単に調整可能

コードと解説

1. CSS

:root {
	--header-height: 4pc;
	--anchor: var(--header-height);
}

@media print, screen and (min-width: 744px) {
	:root {
		--header-height: 5.5pc;
		--anchor: var(--header-height);
	}
}

/* アンカーリンク位置調整(フォールバック用) */
[anchor] {
	margin-top: calc(var(--anchor) * -1);
	padding-top: var(--anchor);
}

:root セレクタでグローバルなCSS変数を定義しています。--header-height はヘッダーの高さ、--anchor はアンカーリンクの調整値です。メディアクエリで画面サイズに応じて値を変更できます。

2. HTML

<!-- デフォルトの調整を使用(--anchor変数が適用される) -->
<section id="about" anchor>
	<h2>About</h2>
	...
</section>

<!-- カスタム値で調整 -->
<section id="service" anchor="120px">
	<h2>Service</h2>
	...
</section>

<!-- 単位省略可(pxとして扱われる) -->
<section id="contact" anchor="100">
	<h2>Contact</h2>
	...
</section>

<!-- CSS変数を直接指定 -->
<section id="news" anchor="var(--header-height)">
	<h2>News</h2>
	...
</section>

<!-- IDなしでもOK(自動生成される) -->
<hr anchor="5pc">

anchor 属性を追加するだけで、アンカーリンクの位置調整が適用されます。値を指定すれば個別のオフセットも可能です。[^1]

3. JavaScript

(function () {
	//_instyle-anchor.js
	//[anchor] 属性を持つすべての要素を取得
	const elements = document.querySelectorAll('[anchor]');

	//スタイル要素を作成(一度だけ作成)
	const STYLE = document.createElement('style');
	document.head.appendChild(STYLE);

	elements.forEach((el) => {
		//anchor 属性から値を取得
		const anchorValue = el.getAttribute('anchor');

		//IDを取得、なければランダムIDを生成
		let id = el.id || 'anchor-' + Math.random().toString(36).substr(2, 9);
		el.id = id;

		let anchorStyle = '';

		if (anchorValue) {
			//値と単位を分離する正規表現('r'も含める)
			const match = anchorValue.match(/^(\d*\.?\d+)(px|pc|r|rem|vw|vh|em)?$/);

			if (match) {
				const value = parseFloat(match[1]);
				let unit = match[2] || 'px'; //単位が指定されていない場合はpxとする

				//'r' を 'rem' に変換
				if (unit === 'r') {
					unit = 'rem';
				}

				//CSSルールを生成(数値+単位の場合)
				anchorStyle = `
				#${id} {
					margin-top: -${value}${unit};
					padding-top: ${value}${unit};
				}
				`;
			} else {
				//数値・単位でない場合(CSS変数など)はそのまま使用
				anchorStyle = `
				#${id} {
					margin-top: calc(-1 * (${anchorValue}));
					padding-top: ${anchorValue};
				}
				`;
			}
		} else {
			//属性値がない場合はCSS変数をフォールバック
			anchorStyle = `
			#${id} {
				margin-top: calc(-1 * var(--anchor));
				padding-top: var(--anchor);
			}
			`;
		}

		//スタイルに追加(エラーハンドリング付き)
		try {
			STYLE.sheet.insertRule(anchorStyle, STYLE.sheet.cssRules.length);
		} catch (e) {
			console.error(`Failed to insert CSS rule for element with id "${id}": ${e.message}`);
		}
	});
})();

JavaScriptの動作

  1. anchor 属性を持つすべての要素を選択
  2. 新しい <style> 要素を作成
  3. 各要素に対して:
    • anchor 属性の値を取得
    • IDがなければランダムIDを自動生成
    • 値の形式に応じてCSSルールを生成
    • 生成したルールをスタイルシートに挿入

対応する値の形式

指定方法 例 結果
数値+単位 anchor="120px" margin-top: -120px; padding-top: 120px;
数値のみ anchor="100" margin-top: -100px; padding-top: 100px;
rem省略形 anchor="5r" margin-top: -5rem; padding-top: 5rem;
CSS変数 anchor="var(--header-height)" CSS変数がそのまま適用
値なし anchor var(--anchor) が適用

まとめ

scroll-padding-top はシンプルで便利なプロパティですが、Scroll Restoration(ブラウザのスクロール位置復元機能)以外にも「フォーカス移動時のスクロール」「Safari の rem 解釈」など、複数の落とし穴があります。

CSS変数とJavaScriptを組み合わせた手法なら:

  • リロード時のズレが発生しない
  • 個別のアンカーポイントごとに異なるオフセット値を設定できる
  • レスポンシブ対応も容易
  • HTMLの属性で直感的に調整できる

固定ヘッダーを使用するレスポンシブデザインのウェブサイトで特に有用な手法です。



参考

[^1]: 厳密には、data-anchor の方がいいかもしれません。