1. ホーム
  2. CSS

【CSS】scroll-behavior の使い方|JavaScriptなしでスムーズスクロールを実装する方法

Share

ページ内リンクをクリックしたとき、目的の位置までスッと滑らかに移動する。以前は 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 要素に指定する方法です。これだけで、ページ内アンカーリンクでの移動がすべて滑らかになります。

style.css
html {
  scroll-behavior: smooth;
}

あとは、いつも通りのアンカーリンクを置くだけです。リンク先の href#id を書き、飛ばしたい要素に同じ id を付けます。

index.html
<a href="#contact">お問い合わせへ</a>

<!-- ページ下部 -->
<section id="contact">
  <h2>お問い合わせ</h2>
</section>

これで、リンクを押すと #contact の位置まで滑らかにスクロールします。JavaScript は一切書いていません。ページ先頭に戻すには href="#top" のように書いておき、対応する id="top" を先頭付近に置くとよいでしょう。

特定のコンテナだけスムーズにする

scroll-behavior は、独自にスクロールできる要素(overflow: autooverflow: 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;
  }
}
Preview

このように、スクロールを担っている要素に指定するのがポイントです。枠の中身ではなく、overflow でスクロールバーが出る要素側に scroll-behavior を書きます。

固定ヘッダーで見出しが隠れるときの対処

画面上部に固定ヘッダー(position: fixedsticky)があると、アンカーリンクで飛んだときに目的の見出しがヘッダーの裏に潜り込んでしまうことがあります。ブラウザは要素の上端が表示領域の上端に来るようにスクロールするため、その上に重なるヘッダーの分だけ位置がずれるわけです。

これを解決するのが scroll-margin-topscroll-padding-top です。どちらもスクロールの停止位置をずらすためのプロパティで、指定する場所が異なります。

プロパティ指定する場所と効果
scroll-margin-topスクロール先の各要素(ターゲット)に付ける。その要素の上に余白を確保し、停止位置を下げる
scroll-padding-topスクロールコンテナ側に付ける。コンテナ内のすべてのスクロール停止位置を、まとめて下げる

ヘッダーの高さが 64px なら、次のようにその分だけオフセットを付けます。個々のターゲットに付けるなら scroll-margin-top が手軽です。

style.css
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;
  }
}
Preview

固定ヘッダーがあるサイトでは、scroll-behavior: smooth; とセットでオフセットを用意しておくと、リンク移動の使い勝手がぐっと良くなります。

JavaScript の scrollIntoView との関係

JavaScript でスクロールさせる場合、element.scrollIntoView()window.scrollTo() にオプションを渡して滑らかにする方法もあります。behavior: 'smooth' を指定すると、その1回のスクロールだけがアニメーションになります。

main.js
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 で受け取れます。ユーザーが動きを減らす設定にしているときは、スムーズスクロールをオフにしておくのが親切です。

style.css
/* 標準では滑らかにスクロールする */
@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-topscroll-padding-top でオフセットを付け、動きを嫌うユーザーには prefers-reduced-motion で配慮する。この3点を押さえておけば、シンプルで使いやすいスムーズスクロールになります。

参考ページ