GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

紙吹雪 アニメーション css キラキラ演出9選

会員登録や決済が終わった瞬間、画面いっぱいに色とりどりの紙の破片が舞い散る演出を見たことはありませんか。あれは紙吹雪 アニメーション cssと呼ばれるコードで作られていて、ボタンを押す、チェックを入れるといった操作一つで同じお祝いの動きを、一行でコピペOKなコードのまま自分のサイトに持ち込めます。

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

9個は華やかさの順ではなく、実際にサイトで紙吹雪が使われる場面の順に並べています。ボタン一つで弾ける合図(01)と背景で降り続ける合図(02)から、ボタンや見出し文字そのものが光る強調(03〜04)へ進み、より大きなインパクトを与える爆発型の反応(05〜06)を経て、最後はマークアップを増やさない実装(07)、JavaScriptなしでトリガーをかける実装(08)、マウスに追従するインタラクション(09)で締めくくります。色紙が半透明な板の裏を流れるようにするならグラスモーフィズムの効果が背景を与え、紙が弾むならバウンスアニメーションが落下の曲線を与えます。

01ボタンで紙吹雪爆発

いつ使うか

登録や決済を終えたボタンを押すと、あらかじめSCSSの@forループで角度と距離を振り分けた紙吹雪12個が、transform: translate+rotateで四方に飛び出しながら薄れて消えます。

ボタンで紙吹雪爆発
新しいタブで開く

01 · ボタンで紙吹雪爆発 — 計測 累積面積 2.569% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@for 루프translate+rotate900ms

なぜこう書くか

もう一度押すと、クラスを外してから付け直すだけの短いJavaScript7行がアニメーションを最初から再生し直します。

@for $i from 1 through 12 {
  .cb-piece:nth-child(#{$i + 1}) {
    background: nth($piece-colors, ($i - 1) % 4 + 1);
    --angle: #{$i * 30deg};
    --dist: #{64px + ($i % 3) * 22px};
    animation-delay: #{($i % 4) * 40ms};
  }
}

@keyframes cb-fly {
  0%   { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(0) scale(.4); opacity: 0; }
  15%  { opacity: 1; }
  100% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(-1 * var(--dist))) scale(1); opacity: 0; }
}

02紙吹雪の雨

css 背景 紙吹雪 をどこで使うか

当選バナーの上で紙吹雪18個が、それぞれ別々のanimation-durationで速さを変えながら降り注ぎます。

紙吹雪の雨
新しいタブで開く

02 · 紙吹雪の雨 — 計測 累積面積 12.233% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@for 루프translateY2.4s

実装の核

横位置(left)と同じ並び順でtopの開始位置も揃えてしまうと一列に固まった対角線になるため、topの並び順をわざと横位置とずらして散らしています。

$tops: -40px, 160px, -180px, 20px, 100px, -120px, 60px, -200px, 140px, -20px, 180px, -100px, 40px, -160px, 120px, -60px, 200px, -140px;
@for $i from 1 through 18 {
  .cr-piece:nth-child(#{$i + 1}) {
    left: percentage($i / 19);
    top: nth($tops, $i);
    background: nth($piece-colors, $i % 4 + 1);
    animation-duration: #{1700ms + ($i % 5) * 140ms};
  }
}

@keyframes cr-fall {
  0%   { transform: translateY(0) rotate(0deg); }
  100% { transform: translateY(520px) rotate(300deg); }
}

03キラキラボタン

何が動くか

プレミアムボタンにマウスを乗せると、枠の外側にある小さな星4つがそれぞれ異なるanimation-delayで拡大・縮小を繰り返しながら光ります。

キラキラボタン
新しいタブで開く

03 · キラキラボタン — 計測 累積面積 0.213% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleopacitystaggered delay

何が肝心か

マウスを離した瞬間はトランジションを挟まずぴたりと止まるので、画面がちらつきません。

@keyframes sk-twinkle {
  0%, 100% { opacity: 0; transform: scale(.3) rotate(0deg); }
  50%      { opacity: 1; transform: scale(1) rotate(90deg); }
}

// 実運用: ホバー中だけ瞬く。キャプチャ用の is-demo は常時オン
.sk-wrap:hover .sk-star,
.sk-wrap.is-demo .sk-star { animation: sk-twinkle $duration $easing infinite; }
.sk-star--2 { animation-delay: 160ms; }
.sk-star--3 { animation-delay: 320ms; }
.sk-star--4 { animation-delay: 480ms; }

04キラキラ文字

どこで使うか

お祝いの見出し文字の周りに置いた3つのキラキラが、それぞれ別のタイミングで拡大・縮小して点滅します。

キラキラ文字
新しいタブで開く

04 · キラキラ文字 — 計測 累積面積 0.193% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleopacity@keyframes twinkle

押さえておく点

主役はボタンではなく文字そのものなので、白い背景の中でも視線がまず見出しに向かいます。

.st-star {
  position: absolute;
  color: $color;
  font-size: 18px;
  line-height: 1;
  opacity: 0;
  transform: scale(.2);
  pointer-events: none;
}
.st-star--1 { top: -14px; left: -18px; color: $stage-orange; }
.st-star--2 { top: -20px; right: -14px; }
.st-star--3 { bottom: -16px; left: 40%; color: $stage-orange; }

@keyframes st-twinkle {
  0%, 100% { opacity: 0; transform: scale(.2) rotate(0deg); }
  50%      { opacity: 1; transform: scale(1) rotate(70deg); }
}

.st-wrap.is-demo .st-star { animation: st-twinkle $duration $easing infinite; }
.st-star--2 { animation-delay: 260ms; }
.st-star--3 { animation-delay: 480ms; }

05花火ドット

見えるもの

中心の点1つが--angleカスタムプロパティを8等分して持つ点8個に分かれ、rotate(var(--angle))とtranslateYだけで花火のように広がりながら消えていきます。

花火ドット
新しいタブで開く

05 · 花火ドット — 計測 累積面積 1.597% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

custom propertyrotate()+translateY1s

どう作るか

マークアップは空のspan8個だけで、位置の計算はすべてSCSS側が肩代わりします。

// ドット8個が --angle を 30~360deg で分け合い、rotate + translateY で火花のように広がる
@for $i from 1 through 8 {
  .fw-dot:nth-child(#{$i}) {
    background: nth($piece-colors, ($i - 1) % 4 + 1);
    --angle: #{$i * 45deg};
  }
}

@keyframes fw-burst {
  0%   { transform: rotate(var(--angle)) translateY(0) scale(1); opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: rotate(var(--angle)) translateY(-92px) scale(.3); opacity: 0; }
}

06絵文字ポップ

どんな場面か

応援ボタンを押すと絵文字3個が、それぞれ違う遅延でtranslateYとscaleを重ねたキーフレームに乗って跳ね上がり、やがて消えます。

絵文字ポップ
新しいタブで開く

06 · 絵文字ポップ — 計測 累積面積 1.865% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateY+scalestaggered delay700ms

コードの読み方

もう一度押すと、01番と同じ仕組みの短いJavaScriptがアニメーションを再生し直します。

@keyframes ep-pop {
  0%   { transform: translateY(0) scale(.4); opacity: 0; }
  30%  { opacity: 1; }
  100% { transform: translateY(-64px) scale(1.1); opacity: 0; }
}

// 実運用: クリックが is-pop を付け直す (上の <script>)
.ep-wrap.is-pop .ep-emoji,
.ep-wrap.is-demo .ep-emoji { animation: ep-pop $duration $easing infinite; }
.ep-emoji--2 { animation-delay: 110ms; }
.ep-emoji--3 { animation-delay: 220ms; }

07擬似要素だけの紙吹雪

何が起きるか

完了カードのマークアップに要素を一つも追加せず、::beforeと::afterの2つだけにbox-shadowで点を6個ずつ描いて落とします。

擬似要素だけの紙吹雪
新しいタブで開く

07 · 擬似要素だけの紙吹雪 — 計測 累積面積 2.233% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

::before/::afterbox-shadow 다중점1.2s

コードの要点

二つの疑似要素は開始位置と遅延だけを変えてあるので、重なって見えることはありません。

// マークアップに要素は一つも足さない — ::before·::after それぞれ box-shadow でドット6個ずつを描く
.pc-card::before,
.pc-card::after {
  content: "";
  position: absolute;
  top: -18px;
  left: 14px;
  width: 6px;
  height: 6px;
  border-radius: $r-pill;
  background: $color;
  box-shadow:
    22px -8px 0 -1px $stage-yellow,
    44px 6px  0 0  $stage-orange,
    66px -12px 0 -1px $subject-cream,
    88px 4px  0 0  $color,
    110px -6px 0 -1px $stage-yellow,
    132px 8px 0 0  $subject-cream;
  opacity: 0;
}

08:has(:checked)でお祝いトリガー

画面で起きること

チェックボックスを押して「やることリスト」の完了を示すと、JavaScriptなしで親のlabelが:has(.hc-check:checked)で状態を読み取り、紙吹雪8個をまとめて発動させます。

:has(:checked)でお祝いトリガー
新しいタブで開く

08 · :has(:checked)でお祝いトリガー — 計測 累積面積 1.599% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

:has():checked800ms

動く仕組み

チェックを外すとトリガーの条件が偽に戻るため、keyframesは再び発動しません。

@keyframes hc-fly {
  0%   { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(0) scale(.4); opacity: 0; }
  20%  { opacity: 1; }
  100% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(-58px) scale(1); opacity: 0; }
}

.hc-wrap:has(.hc-check:checked) .hc-piece,
.hc-wrap.is-demo .hc-piece { animation: hc-fly $duration $easing infinite; }

09カーソルキラキラトレイル

見え方

バナーの上でマウスを動かすと、その座標が--mx・--myに保存され、6つのキラキラがそれぞれ自分のオフセットを足したtranslate()でその点を追いかけながら点滅します。

カーソルキラキラトレイル
新しいタブで開く

09 · カーソルキラキラトレイル — 計測 累積面積 1.717% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pointermovecustom propertytranslate()

組み立て方

--mx・--myを@propertyで<length>として登録しておかないと、translate()の値が滑らかに補間されずカクついて動きます。

@keyframes sc-twinkle {
  0%, 100% { transform: translate(calc(var(--mx) + var(--ox)), calc(var(--my) + var(--oy))) translate(-50%, -50%) scale(0); opacity: 0; }
  50%      { transform: translate(calc(var(--mx) + var(--ox)), calc(var(--my) + var(--oy))) translate(-50%, -50%) scale(1); opacity: 1; }
}

// キャプチャ用: is-demo は --mx·--my 自体が円を描いて回る (ヘッドレスは pointermove を出せない)
@keyframes sc-drift {
  0%   { --mx: 44px;  --my: 40px; }
  25%  { --mx: 236px; --my: 34px; }
  50%  { --mx: 228px; --my: 140px; }
  75%  { --mx: 52px;  --my: 132px; }
  100% { --mx: 44px;  --my: 40px; }
}

.sc-wrap.is-demo { animation: sc-drift 2s linear infinite; }
.sc-wrap .sc-piece { animation: sc-twinkle $duration $easing infinite; }

紙吹雪アニメーションはどこで壊れるか

何が問題か

css キラキラ エフェクト も含む9個の中で、最初につまずいたのは02番でした。紙吹雪18個にanimation-delayだけを変えて出発をずらしたところ、topの開始位置が全部そろっていたせいで一列に固まったまま同じ対角線を描いて落ちてしまいました。横位置(left)の並び順とは違う順番でtopの開始値を散らして、ようやく本物の雨のように見えるようになりました。もう一つ気づきにくかったのがanimationのショートハンドです。

どう避けるか

淡い色の上でさらに淡い紙が消える問題は、パステル調のLPキットが明度の段を層に重ねて解くまさにその地点です。

まとめてanimation: cr-fall $duration $easing infinite;のように書いてしまうと、その行はnth-childで1つずつ指定したanimation-durationより詳細度が高くなるため、せっかく散らした速さをすべて同じ値で上書きしてしまいます。02番ではanimation-name・animation-timing-function・animation-iteration-countだけをまとめて指定し、animation-durationはnth-child側に残すことで、18個それぞれの速さを保っています。08番の:has()と09番の@propertyも比較的新しい機能で、Chrome・Edge・最新のSafariとFirefoxでは問題なく動きますが、対応していない古いブラウザではトリガーそのものが発動しなかったり、座標が滑らかに補間されずカクついたりします。zipを解凍するときに求められるパスワードはbjwrdptnで、この段落の文章をゆっくり読めば見つかります。

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

reduced-motion で残るもの

OSの「視差効果を減らす」設定がオンになっていると、9個すべてでprefers-reduced-motion: reduceのメディアクエリが働き、アニメーションとトランジションが止まります。01・02・05・06・07のような装飾目的の紙吹雪は、止まった状態のままでも情報が欠けることはありません。03・04の点滅や09番のカーソルトレイルも、動いているかどうか自体が意味を持つわけではないので、消してもボタンや文字の内容はそのまま読めます。08番は:has(:checked)がアニメーションと関係なく状態を読み取るので、動きを減らしてもチェックボックスの完了表示そのものは消えません。

あわせて読みたい

似たようなカード型の反応を探しているならCSSベントグリッドホバーアニメーション9選もあわせてご覧ください。ほかのCSSアニメーションのまとめはCSSカテゴリーから探せます。

FAQ — よくある質問

css 紙吹雪 はJavaScriptがないと使えませんか?

いいえ、08番の:has(:checked)のようにチェックボックスの状態だけでトリガーをかける項目はJavaScriptが一行も要りません。05番の花火ドットや07番の疑似要素の紙吹雪もCSSだけで自動的に繰り返されます。01・06・09番のようにクリックやマウス座標が必要な項目だけ、短いJavaScriptを使います。

紙吹雪の個数を増やすと重くなりますか?

12〜18個程度ならtransformとopacityだけを動かしているのでレイアウトの再計算が発生せず、ほとんどの端末で滑らかに動きます。ただし数十個以上に大きく増やすと合成レイヤーが増え、性能の低い端末では体感のフレームレートが落ちることがあるため、画面に同時に見える個数は20個前後を目安にするのがおすすめです。

zipの中のvanillaとReactのコードは動きが違いますか?

いいえ、見た目の動きは同じです。vanilla側はSCSS変数($duration・$easing・$colorなど項目ごとに対応する変数)をファイル冒頭にまとめており、React版ではその値をコンポーネントのpropsとして受け取り、内部でCSSカスタムプロパティに変換して渡す仕組みだけが異なります。お祝いの文面が一文字ずつ現れる必要があるなら、その方法はCSSテキスト演出の9選に集まっています。

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

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