【コピペOK】CSS ボタン アニメーション 9選(SCSS + React)
CSS ボタン アニメーションは、ボタンが操作に「反応した」ことを伝える小さな動きです。ホバー、押下、待機、失敗、確定。この記事ではこうした状況を9つ選び、それぞれ transform と opacity だけで実装しました。グリッドで触ってみて、気に入ったものだけでも、9つまとめてでも、1つの zip でダウンロードできます。
クリックで発動 · すべて1つのzip
並び順は「人気順」ではなく、ボタンが実際に通る順番です。まず目に留まり(01〜05)、押されたら応え(06〜07)、失敗したら伝え(08)、危ない操作はもう一度確かめる(09)。各項目には要点となるコードだけを載せ、完全なファイルは zip に入っています。
01リップル
押した位置から円形の波が広がって消えます。決済や確認のように「確かに押した」という手応えが必要なボタンに向いています。
.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 浮き上がり、影が広がります。カード型ボタンや「カートに入れる」など、つかめそうな質感を出したいところに向いています。
$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下線スライド
下線が左から引かれ、カーソルが離れると右へ抜けていきます。テキストリンクやナビゲーションに向いています。
.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つ置くとおもちゃっぽく見えてしまいます。
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つに絞って使ってください。
.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ローディング切替
押すと文字が消えてスピナーが現れます。保存や送信など待たせる操作に使い、幅は固定して隣の要素を動かさないのが要点です。
.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つだけで作るのでアイコンフォントは不要です。
.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回以上揺らすと怒っているように見えます。
.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 側では pointerdown で is-holding を付与し、pointerup で外して、塗りつぶしの animationend を実行の合図にします。
.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つすべてが transform と opacity だけを動かすのには理由があります。width や top をアニメーションするとブラウザは毎フレームレイアウトを計算し直しますが、transform と opacity はコンポジット(合成)の段階だけで処理されます。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 で渡すだけです。
アニメーションがカクつきます。
まず transform と opacity 以外のプロパティをアニメーションしていないか確認してください。次に will-change: transform を動く要素だけに付けてください。全部に付けると逆に遅くなります。
コメント
コメントは近日公開。それまではお問い合わせページへ。