GODRICH

【コピペOK】料金表 CSSアニメーション9選

サブスクの料金プランを比較するとき、カードにマウスを乗せた瞬間にふわっと浮かんだり、金額の表示がパッと切り替わったりするページを見たことはありませんか。どのプランを選ぶか迷っているその一瞬に、こうした小さな動きが視線を誘導してくれます。

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

9個は人気順ではなく、訪問者が料金表を見ていく順番に並べています。まずスクロールして画面に入ってきたときに自然と目に留まる01〜03、次に実際に指を動かして支払い周期を切り替える04〜06、そのあとプランに含まれる機能を確認したり他のプランと見比べたりする07〜08、最後に申し込みボタンへ視線を導く09という流れです。

01スクロールでプランがリビール

料金表のあるページをスクロールしていくと、プランのカードが一度に出てくるのではなく順番に一枚ずつ浮かび上がって表示されます。IntersectionObserverがグリッド全体の画面到達を検知してinクラスを付け、カードごとにtransition-delayだけを変えることで同じ動きを時間差で繰り返しています。

IntersectionObservertransition-delayJS 7줄
var grid = document.querySelector('.sr-grid');
var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    if (e.isIntersecting) { grid.classList.add('in'); io.unobserve(grid); }
  });
}, { threshold: 0.3 });
io.observe(grid);

02バッジ光沢スイープ

「人気」バッジの上を細い光の帯が数秒おきに流れていき、マウスを乗せなくても自然と目が向きます。バッジにoverflow: hiddenをかけたうえで内側のグラデーションの帯だけをtransform: translateXで動かしているため、バッジ自体の大きさは変わりません。

::aftertranslateX2.4s
.sh-badge { position: relative; overflow: hidden; }
.sh-badge::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 40%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .65), transparent);
  animation: sh-sweep 2.4s ease-in-out infinite;
}
@keyframes sh-sweep {
  0% { transform: translateX(-260%) skewX(-12deg); }
  60%, 100% { transform: translateX(360%) skewX(-12deg); }
}

03人気プランの浮き上がり強調

枠と色ですでに強調されている人気プランのカードにマウスを乗せると、カードがもう一段浮き上がり影が深くなります。動かしているのはtransform: translateYbox-shadowだけなので、隣に並ぶ他のカードは影響を受けません。

translateYbox-shadow240ms
.pl-card--popular {
  box-shadow: $shadow-press;
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.pl-card--popular:hover {
  transform: translateY(-16px);
  box-shadow: $shadow-raised;
}

04月額/年額トグル

MonthlyとYearlyのボタンの間を、ピル形の背景がなめらかに移動して今どちらを選んでいるかを示します。隠しラジオボタンの:checked状態を兄弟要素セレクターで読み取って背景を動かしているだけなので、JavaScriptは一行も使っていません。

:checkedtranslateX50%
.mt-indicator {
  position: absolute;
  width: calc(50% - #{$sp-1});
  height: calc(100% - #{$sp-1} * 2);
  transition: transform $duration $easing;
}
#mt-yearly:checked ~ .mt-indicator { transform: translateX(100%); }

05価格数字フリップ

価格の数字にマウスを乗せると、カードのように回転して裏側に書かれた別の金額が現れます。perspectiveを与えた親要素の中でbackface-visibility: hiddenを持つ二枚の面を重ねておき、内側のレイヤーだけをrotateXで回転させる仕組みです。

rotateXbackface-visibilityperspective
.pf-flip__inner {
  transform-style: preserve-3d;
  transition: transform $duration $easing;
}
.pf-flip:hover .pf-flip__inner { transform: rotateX(180deg); }
.pf-flip__face { position: absolute; inset: 0; backface-visibility: hidden; }
.pf-flip__face--back { transform: rotateX(180deg); }

06:has(:checked)でJS0価格切替

見た目は04のトグルと似ていますが、こちらは隠しラジオボタンの状態をカード全体から:has()で読み取り、月額・年額二つの価格行をopacitytranslateYでクロスフェードさせます。値を入れ替える処理がセレクター一つで完結するため、JavaScriptを足す必要がありません。

:has():checkedopacity
.hc-price { position: absolute; inset: 0; opacity: 0; transform: translateY($sp-2); }
.hc-price--monthly { opacity: 1; transform: none; }
.hc-card:has(.hc-radio--y:checked) .hc-price--monthly {
  opacity: 0;
  transform: translateY(-#{$sp-2});
}
.hc-card:has(.hc-radio--y:checked) .hc-price--yearly { opacity: 1; transform: none; }

07機能チェックが順番に表示

カードにマウスを乗せると、含まれる機能一覧のチェック印が上から一行ずつ時間差をつけて現れます。チェックのアイコンは線を二本別々に描くのではなく、一つの要素をborder-rightborder-bottomでL字に組んでから45度回転させているので、線の継ぎ目がずれる心配がありません。

transition-delayopacitytranslateX
.fc-check {
  width: 6px;
  height: 10px;
  border-right: 2px solid $color;
  border-bottom: 2px solid $color;
  transform: rotate(45deg);
}
.fc-row { opacity: 0; transform: translateX(-#{$sp-3}); transition: opacity 320ms $ease-out, transform 320ms $ease-out; }
.fc-card:hover .fc-row { opacity: 1; transform: none; }
.fc-card:hover .fc-row:nth-child(2) { transition-delay: 120ms; }
.fc-card:hover .fc-row:nth-child(3) { transition-delay: 240ms; }

08比較列ハイライト

複数のプランを並べた比較表で、マウスを乗せた列だけ枠が明るくなりわずかに拡大され、今どの列を見ているかがはっきりします。04や06のような:has()は使わず、素直な:hoverだけで一列ずつ切り替わる作りです。

scaleborder-color180ms
.ch-col {
  border: 2px solid transparent;
  transition: transform 180ms $ease-out, border-color 180ms $ease-out, background 180ms $ease-out;
}
.ch-col:hover {
  transform: scale(1.05);
  border-color: $color;
  background: rgba(23, 20, 26, .08);
}

09CTAグロー

決済ボタンの背後で柔らかい光が膨らんではしぼみを繰り返し、クリックする場所を静かに示します。02番のバッジ光沢スイープと同じくマウスを乗せなくても常に動いている項目で、scaleopacityだけを1.8秒周期でゆっくり変化させています。

radial-gradientscale1.8s
.cg-btn::before {
  content: "";
  position: absolute;
  inset: -#{$sp-3};
  background: radial-gradient(circle, rgba(47, 109, 246, .9), transparent 65%);
  filter: blur(3px);
  animation: cg-pulse 1.8s $ease-spring infinite;
}
@keyframes cg-pulse {
  0%, 100% { transform: scale(1); opacity: .2; }
  50% { transform: scale(1.45); opacity: .95; }
}

料金表アニメーションはどこで壊れるか

最初につまずいたのは09番でした。光をボタンの後ろへ送ろうと何気なくz-index: -1を指定したところ、.cg-btn自体にはz-indexの指定がなく新しいスタッキングコンテキストが生まれないため、その-1は一番近い祖先であるステージの背景よりもさらに下へ適用され、光が完全に見えなくなってしまいました。累積面積も強度も0のまま測定され、原因はすぐには分かりませんでした。z-indexの指定を外すと、::beforeはボタン自身の背景の上、つまり本来あるべき場所に戻り、数値も正常に測定されました。06番で使った:has()セレクターにも注意点があり、ChromeやEdge、最新のSafariとFirefoxでは問題なく動きますが、対応していない古いブラウザはこの規則ごと無視するため、価格の表示は月額のまま切り替わらずに止まります——数値そのものが崩れるわけではなく演出だけが消える程度なので、実務上の影響は大きくありません。zipを解凍するときに求められるパスワードはprqmpcmjで、この段落をゆっくり読めば見つかります。奥行きを出すためだけにz-indexを書くクセがある人は、負の値を使う前にその要素へposition以外のスタッキングの根拠——isolation: isolatez-index: 0——があるかを先に確かめておくと、同じ失敗を避けられます。

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

OSで動作を減らす設定をオンにすると、9個ともprefers-reduced-motion: reduceでアニメーションとトランジションが止まります。02番のバッジスイープと09番のCTAグローのようにホバーなしで常に動いていた項目は、光が止まった状態で表示されても情報が欠けることはありません。01番と07番のように現れること自体が情報になっている項目は、動きの代わりに最初からopacity: 1に固定され、カードとチェック一覧がそのまま全部見える状態になります。04・05・06番は切り替わった後の値(月額・年額の金額)が重要なので、遷移の速さだけを消して最終状態はそのまま見せています。

ラジオボタンと:checkedで状態を切り替える別の例はCSSトグルスイッチの記事にもまとめてあり、一覧はCSSカテゴリーから見られます。:has()の対応状況はMDNのドキュメントで確認できます。

FAQ

料金表にアニメーションを付けると本当に成約率が上がりますか?

アニメーションそのものが購入を決めるわけではありません。ただ、人気プランや決済ボタンのようにもともと目立たせたかった要素に自然な動きを添えると、そこへ視線を先に誘導する助けにはなります。

04番のトグルと06番の:has()、どちらを使えばいいですか?

金額はそのままでボタンの形だけ切り替わればよい単純な画面なら04番のトグルで十分です。月額・年額の数字自体を同じ場所で本当に入れ替えたいなら、06番のように:has()で値の切り替えまでCSSの中で終わらせるとJavaScriptを一行も増やさずに済みます。

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

画面に見える動きは同じです。vanilla側のSCSS変数($duration``$easing``$colorなど項目ごとの対応する変数)を、React版ではコンポーネントのpropsとしてそのまま受け取り、内部でCSSカスタムプロパティに変換して渡す仕組みだけが異なります。

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

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