ページ内リンクをクリックしたとき、目的の位置までスッと滑らかに移動する。以前は JavaScript で書いていたこの動きは、いまは CSS の scroll-behavior プロパティを1行足すだけで実現できます。この記事では scroll-behavior の基本から、固定ヘッダーに隠れないようにするオフセットの付け方、動きを嫌うユーザーへの配慮まで、実際に触れるデモとあわせて解説します。CSS を書き始めた初心者から、JS を減らしたい中級者までが対象です。
目次
scroll-behavior とは何か
scroll-behavior は、スクロール位置が変わるときのアニメーションのしかたを指定する CSS プロパティです。値は auto(初期値)と smooth の2つだけで、smooth にするとスクロール位置が一気に飛ぶのではなく、滑らかに動いて目的地まで移動します。
ここで大切なのは、このプロパティが効くのは「プログラム的に起こされたスクロール」だけだという点です。具体的には、アンカーリンク(#id)での移動や、JavaScript の scrollTo() / scrollIntoView() などによるスクロールが対象になります。マウスホイールやスクロールバーのドラッグといった、ユーザーが自分の手で行うスクロールには影響しません。ユーザー操作のスクロールまで滑らかに(=もったり)してしまう心配はない、と考えて大丈夫です。
| 値 | 効果 |
|---|---|
auto | 初期値。スクロール位置が即座に切り替わる(アニメーションなし) |
smooth | スクロール位置まで滑らかにアニメーションしながら移動する |
ページ全体をスムーズスクロールにする
もっとも手軽なのは、ページ全体のスクロールを担う html 要素に指定する方法です。これだけで、ページ内アンカーリンクでの移動がすべて滑らかになります。
html {
scroll-behavior: smooth;
}
あとは、いつも通りのアンカーリンクを置くだけです。リンク先の href に #id を書き、飛ばしたい要素に同じ id を付けます。
<a href="#contact">お問い合わせへ</a>
<!-- ページ下部 -->
<section id="contact">
<h2>お問い合わせ</h2>
</section>
これで、リンクを押すと #contact の位置まで滑らかにスクロールします。JavaScript は一切書いていません。ページ先頭に戻すには href="#top" のように書いておき、対応する id="top" を先頭付近に置くとよいでしょう。
特定のコンテナだけスムーズにする
scroll-behavior は、独自にスクロールできる要素(overflow: auto や overflow: scroll を持つ要素)に対しても指定できます。サイドバーのメニューや、カード一覧のような部分的なスクロール領域の中でだけ滑らかに動かしたいときに便利です。
下のデモは、高さを固定した枠の中に4つのセクションを並べたものです。上のリンクを押すと、枠の中のスクロールが対応するセクションまで滑らかに移動します。scroll-behavior: smooth; は枠(スクロールコンテナ)そのものに指定しています。
<nav class="nav">
<a href="#top">先頭</a>
<a href="#s1">セクション1</a>
<a href="#s2">セクション2</a>
<a href="#s3">セクション3</a>
</nav>
<div class="scroll-area">
<section id="top" class="panel panel--top">
<h2>先頭</h2>
<p>上のリンクを押すと、対応するセクションまで滑らかにスクロールします。</p>
</section>
<section id="s1" class="panel panel--a">
<h2>セクション1</h2>
<p>scroll-behavior: smooth; がスクロールコンテナに効いています。</p>
</section>
<section id="s2" class="panel panel--b">
<h2>セクション2</h2>
<p>アンカーリンクでの移動がアニメーションになります。</p>
</section>
<section id="s3" class="panel panel--c">
<h2>セクション3</h2>
<p>JavaScript は使っていません。CSS だけの実装です。</p>
</section>
</div>.nav {
display: flex;
gap: 8px;
padding: 10px;
background: #1f2937;
position: sticky;
top: 0;
z-index: 1;
}
.nav a {
color: #fff;
text-decoration: none;
font-size: 14px;
padding: 4px 10px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.12);
}
.nav a:hover {
background: rgba(255, 255, 255, 0.28);
}
/* スクロールコンテナに smooth を指定 */
.scroll-area {
height: 220px;
overflow-y: auto;
scroll-behavior: smooth;
border: 1px solid #d1d5db;
}
.panel {
height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 20px;
box-sizing: border-box;
}
.panel h2 {
margin: 0 0 8px;
font-size: 20px;
}
.panel p {
margin: 0;
font-size: 14px;
line-height: 1.6;
}
.panel--top { background: #eef2ff; }
.panel--a { background: #dcfce7; }
.panel--b { background: #fef9c3; }
.panel--c { background: #fee2e2; }
/* 動きを抑えたいユーザーへの配慮 */
@media (prefers-reduced-motion: reduce) {
.scroll-area {
scroll-behavior: auto;
}
}このように、スクロールを担っている要素に指定するのがポイントです。枠の中身ではなく、overflow でスクロールバーが出る要素側に scroll-behavior を書きます。
固定ヘッダーで見出しが隠れるときの対処
画面上部に固定ヘッダー(position: fixed や sticky)があると、アンカーリンクで飛んだときに目的の見出しがヘッダーの裏に潜り込んでしまうことがあります。ブラウザは要素の上端が表示領域の上端に来るようにスクロールするため、その上に重なるヘッダーの分だけ位置がずれるわけです。
これを解決するのが scroll-margin-top と scroll-padding-top です。どちらもスクロールの停止位置をずらすためのプロパティで、指定する場所が異なります。
| プロパティ | 指定する場所と効果 |
|---|---|
scroll-margin-top | スクロール先の各要素(ターゲット)に付ける。その要素の上に余白を確保し、停止位置を下げる |
scroll-padding-top | スクロールコンテナ側に付ける。コンテナ内のすべてのスクロール停止位置を、まとめて下げる |
ヘッダーの高さが 64px なら、次のようにその分だけオフセットを付けます。個々のターゲットに付けるなら scroll-margin-top が手軽です。
html {
scroll-behavior: smooth;
}
/* 飛び先の見出しに、ヘッダー分の余白を確保する */
:target {
scroll-margin-top: 64px;
}
/* あるいは特定の要素だけに付ける */
h2[id] {
scroll-margin-top: 64px;
}
ページ全体に対してまとめて調整したいときは、スクロールコンテナ(ページ全体なら html)に scroll-padding-top を書くほうがシンプルです。下のデモは、枠の中に固定ヘッダーを置き、コンテナ側に scroll-padding-top: 48px; を指定したものです。リンクを押しても、見出しがヘッダーの下に隠れずに止まります。
<div class="scroll-area">
<header class="bar">固定ヘッダー(高さ48px)</header>
<nav class="links">
<a href="#a">A へ</a>
<a href="#b">B へ</a>
<a href="#c">C へ</a>
</nav>
<section id="a" class="block block--a">
<h2>見出し A</h2>
<p>scroll-padding-top のおかげで、見出しがヘッダーの下に隠れません。</p>
</section>
<section id="b" class="block block--b">
<h2>見出し B</h2>
<p>コンテナ側に scroll-padding-top を指定しています。</p>
</section>
<section id="c" class="block block--c">
<h2>見出し C</h2>
<p>個々の要素に scroll-margin-top を付ける方法もあります。</p>
</section>
</div>.scroll-area {
height: 240px;
overflow-y: auto;
scroll-behavior: smooth;
/* 固定ヘッダーの高さ分だけジャンプ位置を下げる */
scroll-padding-top: 48px;
border: 1px solid #d1d5db;
position: relative;
}
.bar {
position: sticky;
top: 0;
height: 48px;
display: flex;
align-items: center;
padding: 0 16px;
background: #2563eb;
color: #fff;
font-size: 14px;
font-weight: bold;
z-index: 2;
}
.links {
display: flex;
gap: 8px;
padding: 10px 16px;
background: #f3f4f6;
}
.links a {
color: #2563eb;
text-decoration: none;
font-size: 14px;
padding: 4px 10px;
border: 1px solid #2563eb;
border-radius: 4px;
}
.links a:hover {
background: #2563eb;
color: #fff;
}
.block {
height: 200px;
padding: 16px 20px;
box-sizing: border-box;
}
.block h2 {
margin: 0 0 8px;
font-size: 20px;
}
.block p {
margin: 0;
font-size: 14px;
line-height: 1.6;
}
.block--a { background: #dbeafe; }
.block--b { background: #dcfce7; }
.block--c { background: #fef3c7; }
@media (prefers-reduced-motion: reduce) {
.scroll-area {
scroll-behavior: auto;
}
}固定ヘッダーがあるサイトでは、scroll-behavior: smooth; とセットでオフセットを用意しておくと、リンク移動の使い勝手がぐっと良くなります。
JavaScript の scrollIntoView との関係
JavaScript でスクロールさせる場合、element.scrollIntoView() や window.scrollTo() にオプションを渡して滑らかにする方法もあります。behavior: 'smooth' を指定すると、その1回のスクロールだけがアニメーションになります。
const target = document.querySelector('#contact');
// この呼び出しだけ滑らかにスクロールする
target.scrollIntoView({ behavior: 'smooth' });
CSS の scroll-behavior と JS のオプションは、次のように役割が違います。CSS の scroll-behavior は、その要素に対するあらゆるプログラム的スクロールの既定の挙動を決めます。一方、scrollIntoView() の behavior オプションはその呼び出し1回だけの挙動を指定するもので、こちらを渡すと CSS の指定より優先されます。behavior を省いたときは 'auto' 扱いになり、要素に効いている CSS の scroll-behavior がそのまま使われます。
つまり、アンカーリンクでの移動をまとめて滑らかにしたいなら CSS の scroll-behavior: smooth; が手軽で、特定の操作だけ制御したいなら JS 側でオプションを渡す、と使い分けるとよいでしょう。両方を無理に併用する必要はありません。
動きが苦手なユーザーへの配慮
スムーズスクロールのような画面の動きは、人によっては目が疲れたり、めまいのような不快感につながったりします。OS には「視差効果を減らす」といった動きを抑える設定があり、その意思は CSS のメディア特性 prefers-reduced-motion で受け取れます。ユーザーが動きを減らす設定にしているときは、スムーズスクロールをオフにしておくのが親切です。
/* 標準では滑らかにスクロールする */
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
この書き方なら、動きを減らす設定のユーザーには scroll-behavior が適用されず、従来どおり瞬時にジャンプします。反対に、すでに html { scroll-behavior: smooth; } と書いてある場合は、@media (prefers-reduced-motion: reduce) の中で scroll-behavior: auto; に戻す書き方でも同じ効果が得られます。
スムーズにならないときに見るところ
スクロールを担う要素に書けているか
もっとも多いのが、scroll-behavior を書く場所の取り違えです。効かせたいのは、実際にスクロールバーが出ている要素です。ページ全体なら html、部分的な領域なら overflow でスクロールできるようにしているコンテナに書きます。中身の子要素に書いても効きません。html ではなく body に書いてしまい効かないケースもあるので、まずは html に指定してみてください。
手でスクロールしても滑らかにならない
前述のとおり、scroll-behavior はマウスホイールやスクロールバー操作といったユーザー自身のスクロールには効きません。これは仕様どおりの動作です。滑らかになるのはアンカーリンクや JS からのスクロールに限られるので、「ホイールで動かすと滑らかにならない」のは不具合ではありません。
飛び先が固定ヘッダーに隠れる
移動自体は滑らかなのに、着地した見出しがヘッダーの裏に入ってしまうときは、scroll-margin-top または scroll-padding-top でオフセットを付けます。これは scroll-behavior とは別の問題で、スムーズかどうかに関わらず起こります。ヘッダーの高さと同じ値を目安に調整してください。
まとめ
scroll-behavior: smooth; を1行足すだけで、JavaScript なしにページ内リンクのスムーズスクロールが実現できます。効くのはアンカーリンクや scrollIntoView() などのプログラム的スクロールで、ユーザー自身の操作には影響しません。ページ全体なら html、部分領域なら overflow を持つコンテナに指定するのがポイントです。固定ヘッダーで見出しが隠れるときは scroll-margin-top や scroll-padding-top でオフセットを付け、動きを嫌うユーザーには prefers-reduced-motion で配慮する。この3点を押さえておけば、シンプルで使いやすいスムーズスクロールになります。