css イージング関数 9選 — コピペで使えるスプリング・バウンス
ボタンを押す、カードが現れる——動きの速さが一定だと硬く見え、最初は速く途中からゆっくりだったり最後に少し弾んだりすると自然に見えます。この速度変化を4つの数値で指定するのがcss イージング関数のcubic-bezier、跳ねる動きをばね方程式から作るのがlinear()です。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 ease-out-quartカーブ
- 02 ease-in-out-backカーブ
- 03 ease-out-expoカーブ
- 04 linear()でバウンス
- 05 やさしいスプリング
- 06 キビキビしたスプリング
- 07 SCSSイージングmap
- 08 Tailwind v4 --easeトークン
- 09 イージング比較デモ
9つは人気順ではなく、値の扱い方が増えていく順に並べています。画面に現れる要素の減速だけで作れる3つ(01〜03)から始まり、cubic-bezierでは表現できない物理的な弾み3つ(04〜06)へ進み、その値をコードで使い回す2つの方法(07・08)を経て、最後は4つを1画面で見比べるデモ(09)で締めくくります。
01ease-out-quartカーブ
translateXにcubic-bezier(.25, 1, .5, 1)を指定すると、最初から素早く飛び出し、慣性なく目標付近で滑らかに止まります。モーダルやドロワーが開くとき、カードが画面に登場するときに向きます。
$duration: $dur-loop; // 2s
$easing: cubic-bezier(.25, 1, .5, 1); // ease-out-quart
.stage.is-demo .puck {
animation: move $duration $easing infinite alternate;
}
@keyframes move {
from { transform: translate(0, -50%); }
to { transform: translate($travel, -50%); }
}
02ease-in-out-backカーブ
cubic-bezier(.68, -.6, .32, 1.6)はy値が0と1の範囲を外れるため、出発前にわずかに後ろへ引き、到着後に目標を超えてから戻る二段の予備動作が生まれます。トーストやバッジが元気よく登場するとき、遊び心のあるボタンの押し込みに向きます。
$easing: cubic-bezier(.68, -.6, .32, 1.6); // ease-in-out-back
.stage.is-demo .puck {
animation: move $duration $easing infinite alternate;
}
@keyframes move {
from { transform: translate(0, -50%); }
to { transform: translate($travel, -50%); }
}
03ease-out-expoカーブ
cubic-bezier(.16, 1, .3, 1)は最初の数フレームでほとんどの移動を終え、残りは目に見えないほどゆっくり這うように進んで到着を引き延ばします。サイドバーやドロップダウンが勢いよく開くとき、速さを強調したいCTAのホバーに向きます。
$easing: cubic-bezier(.16, 1, .3, 1); // ease-out-expo
.stage.is-demo .puck {
animation: move $duration $easing infinite alternate;
}
@keyframes move {
from { transform: translate(0, -50%); }
to { transform: translate($travel, -50%); }
}
04linear()でバウンス
cubic-bezier()は一度しか上下しない曲線なので、ボールが着地後に何度か弾むような動きは表現できません。Robert PennerのeaseOutBounce公式を25点で標本化してlinear()に渡すと、値が上下を繰り返す本物の弾む曲線になります。カートに入るアイコンや、新着の通知バッジが現れる場面に向きます。
$easing: linear(0, .013, .053, .118, .21, .328, .473, .643, .84, .97, .875, .807, .766, .75, .761, .798, .861, .951, .973, .943, .939, .962, .995, .984, 1);
.stage.is-demo .puck {
animation: drop $duration $easing infinite;
}
@keyframes drop {
from { transform: translate(-50%, 0); }
to { transform: translate(-50%, $drop); }
}
05やさしいスプリング
減衰比.62・角振動数7.0の減衰ばね方程式を23点で標本化したlinear()の値で、目標を8%ほど超えてからすぐに収まります。減衰比が1に近いほどオーバーシュートは小さくなります。設定パネルのスライドインなど、普段使いのUI遷移全般に向きます。
$easing: linear(0, .044, .153, .297, .452, .602, .736, .849, .937, 1.002, 1.045, 1.071, 1.082, 1.083, 1.077, 1.066, 1.053, 1.04, 1.028, 1.018, 1.009, 1.003, 1);
.stage.is-demo .puck {
animation: move $duration $easing infinite alternate;
}
@keyframes move {
from { transform: translate(0, -50%); }
to { transform: translate($travel, -50%); }
}
06キビキビしたスプリング
同じ方程式で減衰比を.35に下げ角振動数を9.0に上げると、目標を30%以上(1.307)超えてから何度か行き来する、弾力のはっきりした値になります。いいねボタンを押したときや、ドラッグ後に手を離して跳ねるカードに向きます。
$easing: linear(0, .055, .197, .394, .612, .824, 1.009, 1.153, 1.25, 1.3, 1.307, 1.281, 1.231, 1.167, 1.099, 1.035, .982, .942, .916, .905, .907, .917, .935, .955, .976, .995, 1);
.stage.is-demo .puck {
animation: move $duration $easing infinite alternate;
}
@keyframes move {
from { transform: translate(0, -50%); }
to { transform: translate($travel, -50%); }
}
07SCSSイージングmap
この記事の曲線値を$easingsというSassマップ1つに名前でまとめ、@mixin ease($name)がmap.getで該当する値を取り出してanimation-timing-functionに差し込みます。編ごとに生の曲線値をコピペし直さずに済み、複数のコンポーネントが同じブランドイージングを共有するデザインシステムに向きます。
@use "sass:map";
$easings: (
"ease-out-quart": cubic-bezier(.25, 1, .5, 1),
"ease-in-out-back": cubic-bezier(.68, -.6, .32, 1.6),
"spring-snappy": linear(0, .055, .197, .394, .612, .824, 1.009, 1.153, 1.25, 1.3, 1.307, 1.281, 1.231, 1.167, 1.099, 1.035, .982, .942, .916, .905, .907, .917, .935, .955, .976, .995, 1),
);
@mixin ease($name, $prop: animation-timing-function) {
#{$prop}: map.get($easings, $name);
}
.stage.is-demo .puck {
@include ease("spring-snappy"); // マップから06番と同じ値を取り出す
}
08Tailwind v4 --easeトークン
:rootに--ease-springというカスタムプロパティを置き、animation 単一指定のイージング位置にvar(--ease-spring)を差します。このzipはTailwindそのものは読み込んでおらず、@theme --ease-*の名前で呼び出すパターンだけを素のCSSカスタムプロパティで再現しています。イージングをユーティリティクラスのように名前で管理したいTailwindプロジェクトの参考になります。
$easing: var(--ease-spring); // 実際に動く値は :root のカスタムプロパティにある
:root {
--ease-spring: cubic-bezier(.34, 1.56, .64, 1);
}
.stage.is-demo .puck {
animation: move $duration $easing infinite alternate;
}
@keyframes move {
from { transform: translate(0, -50%); }
to { transform: translate($travel, -50%); }
}
09イージング比較デモ
quart(01)・back(02)・キビキビしたスプリング(06)・バウンス(04)の4曲線を同じ2秒・同じ距離で並べて動かすと、減速の形の違いが一目で分かります。新しい値は作らず、前の4項目の値をそのまま再利用しています。
.puck--quart { animation: move $duration cubic-bezier(.25, 1, .5, 1) infinite alternate; }
.puck--back { animation: move $duration cubic-bezier(.68, -.6, .32, 1.6) infinite alternate; }
.puck--spring { animation: move $duration linear(0, .055, .197, .394, .612, .824, 1.009, 1.153, 1.25, 1.3, 1.307, 1.281, 1.231, 1.167, 1.099, 1.035, .982, .942, .916, .905, .907, .917, .935, .955, .976, .995, 1) infinite alternate; }
.puck--bounce {
// バウンスだけalternateを使わず、ループのたびに元の位置へ瞬時に戻る
animation: move-once $duration linear(0, .013, .053, .118, .21, .328, .473, .643, .84, .97, .875, .807, .766, .75, .761, .798, .861, .951, .973, .943, .939, .962, .995, .984, 1) infinite;
}
どこで壊れるか — 縦トラックは横より先に溢れる
04番(linear()でバウンス)の落下トラックは、最初132pxではなく180pxで作っていました。480×300のデスクトップ画面ではそれで何の問題もありません。ところがこのサイトのモバイル確認は、幅を320pxまで縮めるとき高さも同じ比率(320×300÷480)で200pxまで一緒に縮めます。縦に積むトラック180pxに上下の余白を足すと200pxを超え、実際にレンダリングして測定すると値が動いたフレームが検出できないほど崩れていました。横に寝かせたトラックは幅320pxの中でまだ余裕がありましたが、縦向きのトラックは高さ200pxという、より狭い予算にすぐ突き当たります。トラックを132pxまで縮めてから、この溢れは消えました。zipを解凍するときに聞かれるパスワードはwnxge56eで、案内された通りに入力すれば開きます。縦方向のアニメーションを組むときは横幅基準のレスポンシブ確認だけで済ませがちですが、今回のように狭いモバイル高さも別枠で測っておく必要があります。同じ理由でタブの切り替え幅を固定値で計算する例はCSS タブ切り替えアニメーション9選にもまとめてあります。
アクセシビリティ
9つともprefers-reduced-motion: reduceではanimation: noneに切り替わり、transformをアニメーションの到着位置の値に固定します。01・02・03・05・06・07・08番はトラックの端(196pxまたは188px)に、04番は落下先(108px)に、09番は4レーンそれぞれの終着点に止まります。動きを止めても、静止画面が移動の途中で引っかかったように見えることはありません。この記事自体は曲線の形の違いを見せるのが目的なので、reduced-motionではその違い自体は伝わりませんが、最終位置の情報は残るのでレイアウトが崩れる心配はありません。他に配布しているCSS作例はカテゴリー一覧にまとめてあり、この配布物の作り方は紹介ページに書いています。
FAQ
cubic-bezier()とlinear()は何が違うのですか?
cubic-bezier()は数値4つで定義される1本の滑らかな曲線で、値が何度も上下する動き(ボールが着地後に何度か弾むような動き)は表現できません。linear()は任意の点をいくつでも並べて曲線を描けるため、この記事の04〜06番のように物理方程式を直接標本化した値もそのまま渡せます。
CSSにスプリング専用のキーワードはありますか?
ありません。spring()のような専用のイージング関数はまだ標準として実装されていません。今スプリングのように見える動きをCSSで作るには、この記事の05・06番のように減衰ばね方程式を自分で計算し、linear()に標本化して渡す方法が現実的です。
linear()やvar()はどのブラウザで使えますか?
linear()はChrome 113・Firefox 112・Safari 16.4以降に実装されていて、2023年12月にはBaseline「主要ブラウザで広く使える」機能になりました。それより古いブラウザではanimation-timing-functionの値として認識されず、その宣言全体が無視されてアニメーションが動かなくなります。一方、var()によるカスタムプロパティの参照はずっと前からほぼ全てのモダンブラウザで使えるため、08番の--ease-springのような書き方自体が壊れる心配はほとんどありません。詳しい対応状況はMDNのlinear()のドキュメントで確認できます。