GODRICH

トップへ戻るボタン css アニメーション9選【CSSだけ・JS不要】

トップへ戻るボタンとは、ページを下まで読んだあとにワンクリックで一番上へ戻してくれる小さなボタンのことです。この記事では「トップへ戻るボタン css」でよく探される9つの動きを、AOS・GSAPなしでそのまま使えるコードとしてまとめました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9つは名前順ではなく、ボタンが画面で経験する順番に並べています。まず現れ方(01〜03)、次に見た目そのものが目を引くアイデア型(04・06)、現れ方のもう一つの型(05)、フッターとぶつからないための振る舞い(07)、マウスを乗せたときの反応(08)、最後にアニメーションを使わずブラウザに任せる方法(09)です。

項目 技法 時間 トリガー
01 スクロールで出現 opacity+translateY 260ms スクロール(JS)
02 矢印バウンス translateY 1.4s 常時
03 進捗リング表示 stroke-dashoffset 2s(デモ) スクロール(JS)
04 ロケット型 translateY+rotate 700ms ホバー・フォーカス
05 横からスライド translateX+@starting-style 320ms スクロール(JS)
06 回るテキスト rotate 8s 常時
07 フッターで停止 translateY 220ms スクロール(JS)
08 ホバーでピル拡張 width 220ms ホバー・フォーカス
09 scroll-behavior (ネイティブスクロール) クリック

01スクロールで出現

ボタンは普段は透明で隠れていて、一定の高さ(240px)を超えてスクロールすると、JavaScript 7行が.is-visibleクラスを付け足し、ボタンがゆっくり浮かび上がります。長いリストやブログ記事の下部でよく使われる出方です。

scroll 이벤트opacity+translateY260ms
$duration: 260ms;
$easing: ease-out;

.totop {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity $duration $easing, transform $duration $easing;
}
.totop.is-visible { opacity: 1; transform: translateY(0); }
var btn = document.querySelector('.totop');
var 閾値 = 240;
function sync() { btn.classList.toggle('is-visible', window.scrollY > 閾値); }
window.addEventListener('scroll', sync, { passive: true });
sync();

02矢印バウンス

ボタンの背景は動かさず、中の矢印アイコンだけが上下にぽんぽんと跳ね続けます。JavaScriptなしでCSSの@keyframesひとつだけで、「押すと上へ行く」ことを無言で伝えます。

translateY$ease-out1.4s
.totop__arrow {
  animation: arrow-bounce 1.4s ease-out infinite;
}
@keyframes arrow-bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}

03進捗リング表示

ボタンの縁をSVGの円が取り囲み、stroke-dashoffsetがスクロールした割合だけリングを満たしていきます。@property --pinherits: trueで登録し、親ボタンで変わった値を子のSVG円がそのまま受け継げるようにしたのがポイントです。

stroke-dashoffset@propertyJS 7줄
@property --p {
  syntax: '<number>';
  initial-value: 0;
  inherits: true;
}
.totop { --p: 0; }
.totop__fill {
  stroke-dasharray: 150.8px;
  stroke-dashoffset: calc(150.8px * (1 - var(--p) / 100));
}
var el = document.querySelector('.totop');
function sync() {
  var 最大 = document.documentElement.scrollHeight - window.innerHeight;
  var 割合 = 最大 > 0 ? Math.min(100, (window.scrollY / 最大) * 100) : 0;
  el.style.setProperty('--p', 割合);
}
window.addEventListener('scroll', sync, { passive: true });

04ロケット型

ボタン自体がロケットのアイコンで、マウスを乗せるかキーボードでフォーカスすると機体が傾いてわずかに浮き上がり、後ろの炎が一瞬だけ点滅します。遊び心が欲しいゲーム・スタートアップのランディングページに向いています。

rotate+translateYopacity 깜빡임700ms
.totop:hover .totop__rocket,
.totop:focus-visible .totop__rocket { animation: rocket-launch 700ms ease-out 1; }
.totop:hover .totop__flame,
.totop:focus-visible .totop__flame { animation: flame-flicker 700ms ease-out 1; }

@keyframes rocket-launch {
  0%   { transform: translateY(0) rotate(0deg); }
  40%  { transform: translateY(-9px) rotate(-9deg); }
  100% { transform: translateY(-2px) rotate(-4deg); }
}
@keyframes flame-flicker {
  0%, 100% { opacity: 0; transform: scaleY(.6); }
  40%      { opacity: 1; transform: scaleY(1.3); }
  70%      { opacity: .7; transform: scaleY(.9); }
}

05横からスライド

ボタンが画面右の外側から内側へ滑り込むように現れます。display: noneから切り替わる瞬間の開始値を指定する@starting-styleを使っているので、JavaScriptはスクロール位置を確認するだけで、現れ方そのものはCSSがすべて受け持ちます。

translateX@starting-style320ms
.totop {
  display: none;
  opacity: 0;
  transform: translateX(40px);
  transition: opacity 320ms, transform 320ms, display 320ms allow-discrete;
}
.totop.is-visible {
  display: grid;
  opacity: 1;
  transform: translateX(0);
  @starting-style { opacity: 0; transform: translateX(40px); }
}

06回るテキスト

ボタンの縁に沿って「SCROLL TO TOP」の文字がSVGのtextPathで円形に配置され、ゆっくり回り続けます。中央の矢印だけは別の要素なので回らずに固定されたままです。ブランドの個性を出したいサイトやポートフォリオの下部に合います。

SVG textPathrotate8s
.totop__ring {
  animation: ring-spin 8s linear infinite;
}
@keyframes ring-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
<text><textPath href="#ring-path">SCROLL TO TOP · SCROLL TO TOP · </textPath></text>

07フッターで停止

普段は画面に貼り付いたまま浮いていて、フッターが近づくとそれを隠さないようにtranslateYで位置を少し持ち上げて止まります。IntersectionObserver9行がフッターと重なった高さをCSS変数--liftに代入するだけで、動き自体はtransformひとつのままです。

IntersectionObservertranslateYJS 9줄
.totop {
  transform: translateY(calc(-1 * var(--lift, 0px)));
  transition: transform 220ms ease-out;
}
var btn = document.querySelector('.totop');
var footer = document.querySelector('footer');
new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    var 重なり = Math.max(0, e.intersectionRect.height);
    btn.style.setProperty('--lift', 重なり + 'px');
    btn.classList.toggle('is-near-footer', e.isIntersecting);
  });
}, { threshold: [0, 0.01, 1] }).observe(footer);

08ホバーでピル拡張

普段は丸いアイコンボタンですが、マウスを乗せると横に伸びて「トップへ」の文字が一緒に現れます。widthの切り替えなのでtopleftより周りのレイアウトへの影響はやや大きいものの、動くのはボタン1つだけなので負荷は小さく済みます。

width transition:hover·:focus-visible220ms
.totop {
  width: 48px;
  overflow: hidden;
  transition: width 220ms ease-out;
}
.totop:hover,
.totop:focus-visible { width: 132px; }

09scroll-behaviorでなめらか移動

これまでの8つと違い、この項目にはアニメーションもJavaScriptも登場しません。htmlscroll-behavior: smoothを1行足すだけで、アンカーリンクを押した画面はブラウザが自動でなめらかに上まで動かしてくれます。

href=#mp-topJS 0줄앵커 링크
html { scroll-behavior: smooth; }

.totop {
  text-decoration: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}
<a href="#top" class="totop" aria-label="トップへ">
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <polyline points="5,15 12,8 19,15" />
  </svg>
</a>

どこで壊れるか — @propertyを親だけに書くと子が読めない

03の進捗リング表示を最初に作ったとき、--pinherits: falseで登録してしまい、累積面積が0.00%で丸ごと失敗しました。アニメーション自体は親ボタン(.totop)で回っているのに、実際にリングを描くstroke-dashoffsetは子のSVG円(.totop__fill)にあり、inherits: falseだと子が親の変化した値を受け継げず、リングがずっと0%のまま止まっていたのです。inherits: trueに直すと、その場ですぐ動き出しました。07のフッターで停止にも似た落とし穴があり、デモは実際のスクロールを使わずにfooter-mockとボタンを同じ2秒周期の@keyframesで同時に動かす必要があるため、片方でもタイミングがずれるとボタンがフッターを突き抜けて見えてしまいます。こうしたつまずきを直したあと、9個をまとめた圧縮ファイルを開く鍵は9sbsj52dという場所に、太字にも枠にもせずそっと置いてあるので、流し読みせずゆっくり読むと見つかります。

アクセシビリティ — 9個がreduced-motionで残すもの

OSの動作を減らす設定が入っている場合、9個すべてで@media (prefers-reduced-motion: reduce)が動きを止めます。01・05・07のように現れ方や移動自体が情報である項目は、reduced-motionでも最終的な姿(見えている状態・止まった位置)に固定されるので情報が消えることはありません。02・03・04・06のような常時の演出は、動きを止めてもクリックという機能自体はそのまま残ります。09はscroll-behavior: smoothそのものがすでにブラウザ側でreduced-motionの設定を尊重する値なので、追加の対応は必要ありません。このメディアクエリの対応状況はMDNのprefers-reduced-motionドキュメントで、scroll-behaviorの対応状況はcaniuseで確認できます。

このシリーズの続きはCSSカテゴリーにまとめてあり、スクロールに反応するインジケーターをもっと見たい方はスクロール進捗インジケーター9選もあわせてどうぞ。どんなサイトが作っているのか気になる方はサイト紹介ページに制作方針を書いています。

FAQ

トップへ戻るボタンは何pxスクロールしたら表示すべきですか?

決まった値はありませんが、画面の高さ(ビューポート)の半分ほどでよく使われます。このデモは240pxで固定していますが、実際のページではwindow.innerHeightを基準に計算しておくと、画面サイズが変わっても同じ比率で表示されるようになります。

09のようにscroll-behaviorだけあれば、ほかの項目は不要ですか?

いいえ。scroll-behavior: smoothが決めるのは「どう上へ戻るか」だけで、「ボタンがいつ現れるか」「フッターと重ならないか」といった問題はそのまま残ります。09は01〜08と競合するものではなく、01〜08のどれかで現れ方を決めたうえで、実際のスクロール移動に重ねて使うことが多い組み合わせです。

zipの中のvanillaとreactのコードは違いますか?

画面に見える動きは同じで、値の受け渡し方だけが異なります。vanilla側のSCSS変数3つ($duration$easing$color)を、React版ではコンポーネントのpropsとして受け取り、内部でCSSカスタムプロパティ(--duration--easing--color)に変換して渡しています。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。