GODRICH

【コピペOK】CSS ボタン アニメーション 9選(SCSS + React)

CSS ボタン アニメーションは、ボタンが操作に「反応した」ことを伝える小さな動きです。ホバー、押下、待機、失敗、確定。この記事ではこうした状況を9つ選び、それぞれ transform と opacity だけで実装しました。グリッドで触ってみて、気に入ったものだけでも、9つまとめてでも、1つの zip でダウンロードできます。

クリックで発動 · すべて1つのzip

並び順は「人気順」ではなく、ボタンが実際に通る順番です。まず目に留まり(01〜05)、押されたら応え(06〜07)、失敗したら伝え(08)、危ない操作はもう一度確かめる(09)。各項目には要点となるコードだけを載せ、完全なファイルは zip に入っています。

01リップル

押した位置から円形の波が広がって消えます。決済や確認のように「確かに押した」という手応えが必要なボタンに向いています。

600ms느리게 900ms어두운 물결
.btn {
  position: relative; overflow: hidden; isolation: isolate;
  &::after {
    content: ""; position: absolute;
    left: var(--x, 50%); top: var(--y, 50%);
    width: var(--d, 0); height: var(--d, 0);
    border-radius: 50%; background: $ripple;
    transform: translate(-50%, -50%) scale(0); opacity: 0;
  }
  &.is-rippling::after { animation: ripple $duration $easing forwards; }
}
@keyframes ripple { to { transform: translate(-50%, -50%) scale(1); opacity: 0; } }

02浮き上がり

ホバーで 6px 浮き上がり、影が広がります。カード型ボタンや「カートに入れる」など、つかめそうな質感を出したいところに向いています。

6px높게 10px그림자 없음
$lift: 6px; $duration: 220ms; $shadow: rgba(23, 20, 26, .32);
.btn {
  will-change: transform;
  transition: transform $duration cubic-bezier(.2, .8, .2, 1), box-shadow $duration;
  &:hover, &:focus-visible { transform: translateY(-$lift); box-shadow: 0 14px 24px $shadow; }
  &:active { transform: translateY(-2px); box-shadow: 0 6px 12px $shadow; }
}

03下線スライド

下線が左から引かれ、カーソルが離れると右へ抜けていきます。テキストリンクやナビゲーションに向いています。

2px굵게 3px가운데서
.link::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: $thickness; background: currentColor;
  transform: scaleX(0); transform-origin: right;
  transition: transform $duration cubic-bezier(.2, .8, .2, 1);
}
.link:hover::after { transform: scaleX(1); transform-origin: left; }

04マグネット

カーソルが半径内に入るとボタンがカーソルに引き寄せられます。LP に1つだけ置く CTA 用で、同じ画面に2つ置くとおもちゃっぽく見えてしまいます。

0.35강하게 0.6반경 120px
document.addEventListener('pointermove', function (e) {
  var r = btn.getBoundingClientRect();
  var dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2);
  if (Math.hypot(dx, dy) < radius) btn.style.transform = 'translate(' + dx * strength + 'px,' + dy * strength + 'px)';
  else btn.style.transform = '';
});

05回転ボーダー

グラデーションの枠がボタンの周りを回り続けます。AI 機能やプレミアムなど「これは特別」と伝えたいボタン1つに絞って使ってください。

3s빠르게 1.5s2색
.btn {
  position: relative; overflow: hidden; isolation: isolate;
  padding: $border; border-radius: 16px;
  &::before {
    content: ""; position: absolute; inset: -60%;
    background: conic-gradient(from 0deg, $colors);
    animation: spin $duration linear infinite; z-index: -1;
  }
}
.btn__in { display: block; border-radius: 13px; background: #17141a; }
@keyframes spin { to { transform: rotate(360deg); } }

06ローディング切替

押すと文字が消えてスピナーが現れます。保存や送信など待たせる操作に使い、幅は固定して隣の要素を動かさないのが要点です。

300ms폭 유지체크로 끝
.btn { display: grid; place-items: center; min-width: 200px; }
.btn__label, .btn__spin { grid-area: 1 / 1; transition: opacity $duration, transform $duration; }
.btn__spin {
  width: 22px; height: 22px; border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, .35); border-top-color: #fff;
  opacity: 0; transform: scale(.6); animation: spin $spin linear infinite;
}
.btn.is-loading .btn__label { opacity: 0; transform: translateY(6px); }
.btn.is-loading .btn__spin  { opacity: 1; transform: scale(1); }

07矢印プッシュ

ホバーで矢印の線が伸び、先端が右へ押し出されます。「次へ」「もっと見る」「続ける」に使い、矢印は線1本と回転させた角1つだけで作るのでアイコンフォントは不要です。

8px멀리 14px선 없이
.arrow__line { transform-origin: left; transform: scaleX(.55); transition: transform $duration; }
.arrow__head {
  width: 12px; height: 12px;
  border-top: 2.5px solid currentColor; border-right: 2.5px solid currentColor;
  transform: rotate(45deg); transition: transform $duration;
}
.btn:hover .arrow__line { transform: scaleX(1); }
.btn:hover .arrow__head { transform: rotate(45deg) translate($push * .7, -$push * .7); }

08シェイク

エラー時に左右へ3回揺れて止まります。ログイン失敗や入力エラーに使い、5回以上揺らすと怒っているように見えます。

6px세게 10px두 번만
.btn.is-shaking { animation: shake $duration cubic-bezier(.36, .07, .19, .97) both; }
@keyframes shake {
  10%, 90% { transform: translateX(-$amount * .5); }
  20%, 80% { transform: translateX($amount); }
  30%, 70% { transform: translateX(-$amount); }
  40%, 60% { transform: translateX($amount * .7); }
  50%      { transform: translateX(-$amount * .7); }
}

09長押しで確定

押している間に背景が塗りつぶされていき、塗り切ったら実行、途中で離すと戻ります。削除や退会のように取り消せない操作で、確認モーダルの代わりに使います。JS 側では pointerdownis-holding を付与し、pointerup で外して、塗りつぶしの animationend を実行の合図にします。

1.2s빠르게 0.8s세로로
.btn__fill {
  position: absolute; inset: 0; background: $fill;
  transform: scaleX(0); transform-origin: left; z-index: -1;
  transition: transform 200ms ease-out;
}
.btn.is-holding .btn__fill { animation: fill $hold linear forwards; }
@keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

どこで壊れるか — 落とし穴3つ

9つすべてが transformopacity だけを動かすのには理由があります。widthtop をアニメーションするとブラウザは毎フレームレイアウトを計算し直しますが、transformopacity はコンポジット(合成)の段階だけで処理されます。MDN のアニメーション性能ガイドがこの2つを勧めるのはそのためです。やりがちなのが 06 で、読み込み中にボタンの幅を縮めたくなりますが、その瞬間にこの規則が崩れて隣の要素まで動きます。幅は保ち、中身だけを変えてください。

2つ目は大きさです。最初に 01 を作ったとき、小さなボタンに直径 320px の波を載せたら、2フレームでボタン全体を覆ってしまい、波ではなく「一瞬光った」ように見えました。波の直径はボタンの長辺の2倍で計算し、アニメーション時間は 600ms より短くしないでください。この値はすでに9つのファイルに反映済みで、解凍パスワードは xu8ewsq8 です。下のボタンからダウンロードしてください。

3つ目はタッチです。02・03・07 はホバー前提で、スマホにはホバーがありません。zip のファイルでは同じ動きを :focus-visible にも付けてあり、タッチでは :active がホバーの代わりを務めます。

アクセシビリティ

9つすべてが prefers-reduced-motion: reduce に対応しています。動きは消してもフィードバックは残すのが原則で、02 は浮かずに影だけ、06 は回転しないリング、08 は揺れずに色が変わります。09 だけは例外で、長押し確定は装飾ではなく安全装置なので塗りつぶしを残しています。メディアクエリのブラウザ対応状況は MDN prefers-reduced-motion で確認できます。

@media (prefers-reduced-motion: reduce) {
  .btn.is-demo, .btn.is-shaking { animation: none; }
  .btn:hover { transform: none; }
}

ボタン以外のローディングやトランジションは CSS アニメーションのカテゴリに、このサイトについては紹介ページにまとめています。

FAQ

9つ全部をひとつのプロジェクトに入れてもいいですか?

入れられますが、1画面に2種類までが目安です。04 マグネットと 05 回転ボーダーは「特別なボタン1つ」用なので、1ページに1つまでにしてください。残りは状況ごとに使い分けるものなので、かぶることはほとんどありません。

React ではクラスの切り替えの代わりに何を使いますか?

zip の react/ フォルダのコンポーネントは状態を props で受け取ります。06 は loading、08 は失敗回数の shakeKey、09 は onConfirm コールバックです。SCSS Module は var() しか読まないので、変数3つを props で渡すだけです。

アニメーションがカクつきます。

まず transformopacity 以外のプロパティをアニメーションしていないか確認してください。次に will-change: transform を動く要素だけに付けてください。全部に付けると逆に遅くなります。

コメント

コメントは近日公開。それまではお問い合わせページへ。

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

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