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

トースト通知 css アニメーション9選 — JS最小・CSSだけ

トースト通知 css は、画面の隅に短く現れて自分で消えていく通知カードを、JavaScriptをほぼ使わずtransformとopacityだけで動かす手法です。この記事では9種類を実装し、コードは6〜25行の断片として載せています。

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

9つは目立たせ方の順ではなく、画面に現れてから消えるまでの流れで並べました。登場の型を変えた3つ(01〜03)、閉じ方や消えるきっかけを変えた3つ(04〜06)、状態や環境に応じて見た目を変える3つ(07〜09)という並びです。同じ開閉の文法をナビへ付ければCSSドロップダウンメニューになり、画面の端から押し出す板まで広げればサイドバードロワーになります。

01右からスライドイン

画面右外から滑り込む

カードは画面右外のtranslateX(140%)から0まで滑り込み、そのまま少し止まってから同じ方向へ抜けていきます。実際の表示は0.6秒で、色を示す左側のバーだけは別要素として重ねています。保存やアップロードが終わったことを画面の隅で知らせる、デスクトップ向けの基本形です。

右からスライドイン
新しいタブで開く

01 · 右からスライドイン — 計測 累積面積 19.466% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateX0.6초코너 고정

デスクトップ向けの基本形

.toast {
  transform: translateX(140%);
  opacity: 0;
}
.toast.is-demo { animation: slide-loop $dur-loop $easing infinite; }

@keyframes slide-loop {
  0%   { transform: translateX(140%); opacity: 0; }
  18%  { transform: translateX(0); opacity: 1; }
  78%  { transform: translateX(0); opacity: 1; }
  100% { transform: translateX(140%); opacity: 0; }
}

02Popover APIトースト

「css トースト」をJSなしで開閉する

popover属性とpopovertargetボタンだけで開閉し、ESCや外側クリックでの終了もJSなしで手に入ります。既定では画面中央に固定されるUAスタイルをposition: fixedで上書きし、右上の角に表示させています。ネットワーク状態のようなシステム通知や、軽量なウィジェットに向いています。

Popover APIトースト
新しいタブで開く

02 · Popover APIトースト — 計測 累積面積 23.62% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

popoverpopovertargetshowPopover()

システム通知や軽量ウィジェットに

<div id="toast" class="toast" popover role="status" aria-live="polite">
  <span class="toast__dot" aria-hidden="true"></span>
  <p class="toast__title">接続済み</p>
  <button popovertarget="toast" popovertargetaction="hide" aria-label="通知を閉じる">
    <svg viewBox="0 0 24 24" width="14" height="14">
      <line x1="5" y1="5" x2="19" y2="19"/>
      <line x1="19" y1="5" x2="5" y2="19"/>
    </svg>
  </button>
</div>
<script>document.getElementById('toast').showPopover();</script>

033件スタック

インラインの--iでずらす

3枚のカードにはインラインの--i(0・1・2)が振られていて、普段はtranslateY(calc(var(--i)*-6px))とscaleでわずかにずれて重なっています。hoverまたはfocus-withinで-22px・scale(1)まで開き、3件がそれぞれ読めるようになります。短時間にイベントが重なるダッシュボードの通知トレイに向いています。

3件スタック
新しいタブで開く

03 · 3件スタック — 計測 累積面積 9.192% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CSS 변수 --ihover·focus-within덱 펼치기

ダッシュボードの通知トレイに

.stack__item {
  position: absolute; inset: 0;
  transform: translateY(calc(var(--i) * -6px))
             scale(calc(1 - var(--i) * .05));
  opacity: calc(1 - var(--i) * .22);
}
.stack:hover .stack__item,
.stack:focus-within .stack__item {
  transform: translateY(calc(var(--i) * -22px)) scale(1);
  opacity: 1;
  transition: transform $duration $easing, opacity $duration $easing;
}

04残り時間バー

等速で縮むバーと同じタイミング

下端の進行バーはscaleX(1)からscaleX(0)へ等速で縮み、なくなる瞬間と同じタイミングでトースト本体がopacityとtranslateY(-8px)で消えます。実際の表示は5秒で、進行バーのanimationendをきっかけにJSがトーストを取り除きます。アップロードや送信の完了のように、決まった時間で自動的に閉じる通知に使います。

残り時間バー
新しいタブで開く

04 · 残り時間バー — 計測 累積面積 23.668% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleXlinearanimationend

決まった時間で閉じる通知に

.toast__bar {
  position: absolute; left: 0; bottom: 0;
  width: 100%; height: 4px;
  background: $color;
  transform-origin: left;
}
.toast.is-demo .toast__bar { animation: bar-shrink $dur-loop linear infinite; }

@keyframes bar-shrink {
  0%   { transform: scaleX(1); }
  82%  { transform: scaleX(0); }
  100% { transform: scaleX(0); }
}

05元に戻すトースト

clearTimeoutでタイマーを止める

削除メッセージ横の「元に戻す」ボタンを押すとJSのclearTimeoutでタイマーが止まり、文言が取り消し済みに変わります。押さないまま4秒経つとtransform-origin: topからscaleY(1)が0まで縮み、高さそのものは動かさずに畳まれて消えます。確認ダイアログの代わりに、リストやメール削除の取り消し操作に使います。

元に戻すトースト
新しいタブで開く

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

scaleYclearTimeout4초

削除の取り消し操作に

document.querySelectorAll('.toast').forEach(function (t) {
  var btn = t.querySelector('.toast__undo');
  var msg = t.querySelector('.toast__text');
  var timer = setTimeout(function () {
    t.classList.add('is-gone');
  }, 4000);
  btn.addEventListener('click', function () {
    clearTimeout(timer);
    msg.textContent = '取り消しました';
    t.classList.add('is-cancelled');
  });
});

06スワイプで閉じる

--dxに移動距離を書き込む

ポインターを押したまま動かすとpointermoveが移動距離を--dxに書き込み、translateX(var(--dx))でカードがそのまま追従します。指を離した位置が80pxを超えていればその方向へ滑り出て閉じ、超えなければ0へ戻ります。touch-action: pan-yで縦スクロールを残しているので、モバイルのチャット通知に向いています。

スワイプで閉じる
新しいタブで開く

06 · スワイプで閉じる — 計測 累積面積 20.664% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pointermovetranslateX(var(--dx))80px 문턱

モバイルのチャット通知に

el.addEventListener('pointerdown', function (e) {
  x0 = e.clientX;
  dragging = true;
  el.setPointerCapture(e.pointerId);
});
el.addEventListener('pointermove', function (e) {
  if (!dragging) return;
  dx = e.clientX - x0;
  el.style.setProperty('--dx', dx + 'px');
});
el.addEventListener('pointerup', function () {
  dragging = false;
  if (Math.abs(dx) > 80) {
    el.classList.add(dx > 0 ? 'is-swiped-right' : 'is-swiped-left');
  } else {
    el.style.setProperty('--dx', '0px');
  }
});

07成功アイコン

2枚のSVGをクロスフェード

無彩色のリングと状態色のリング+チェックという2枚のSVGをgrid-area: 1 / 1で重ね、opacityのクロスフェードで入れ替えます。チェック線は同時にstroke-dashoffsetが100から0へ動いて描かれます。$colorをエラー色に替えるだけで、そのまま失敗バッジになります。

成功アイコン
新しいタブで開く

07 · 成功アイコン — 計測 累積面積 0.215% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

opacity 크로스페이드stroke-dashoffset상태색 변수

エラー色に替えるだけで失敗バッジ

.badge__layer { grid-area: 1 / 1; }
.badge__neutral { opacity: .3; }
.badge__colored { opacity: 0; }
.badge__check { stroke-dasharray: 100; stroke-dashoffset: 100; }

.toast.is-demo .badge__neutral { animation: fade-out $dur-loop $easing infinite; }
.toast.is-demo .badge__colored { animation: fade-in $dur-loop $easing infinite; }
.toast.is-demo .badge__check   { animation: draw $dur-loop $easing infinite; }

08モバイル下部トースト

safe-area-inset-bottomを足す

画面下端に固定し、bottomにenv(safe-area-inset-bottom, 0px)を足してノッチやホームインジケーターを避けます。translateY(140%)から0への移動とopacityが同時に動き、ピル型の一行バーが持ち上がって収まります。「トースト 通知 スマホ」で求められる形で、ハンドル付きのボトムシートと違い一行だけ表示してタイマーで自動的に閉じます。

モバイル下部トースト
新しいタブで開く

08 · モバイル下部トースト — 計測 累積面積 15.832% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

safe-area-inset-bottomtranslateY$r-pill

ボトムシートとの違い

.toast {
  position: fixed;
  left: 16px; right: 16px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
  margin: 0 auto;
  max-width: 340px;
  border-radius: 999px;
  transform: translateY(140%);
  opacity: 0;
}
.toast.is-demo { animation: bottom-loop $dur-loop $easing infinite; }

09通知バッジ

先端を軸に減衰しながら回る

ベルの<g>にtransform-box: fill-boxとtransform-origin: 50% 0%を与え、先端を軸に左右へ減衰しながら回転させます。右上のバッジは同じタイミングでscaleが脈打つように拡大・縮小します。ヘッダーの通知アイコンや未読件数の表示に使います。

通知バッジ
新しいタブで開く

09 · 通知バッジ — 計測 累積面積 0.469% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

transform-box: fill-boxrotatescale

ヘッダーの未読件数に

.bell__body {
  transform-box: fill-box;
  transform-origin: 50% 0%;
}
.bell.is-demo .bell__body { animation: shake $dur-loop $easing infinite; }
.bell.is-demo .badge       { animation: pulse $dur-loop $easing infinite; }

@keyframes shake {
  0%, 55%, 100% { transform: rotate(0deg); }
  60% { transform: rotate(-14deg); }
  65% { transform: rotate(12deg); }
  70% { transform: rotate(-8deg); }
}

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

grid-areaを書かないと縦に並ぶ

9つとも壊れやすい場所が違いました。1つ目は07の成功アイコンで、無彩色リングと状態色リング+チェックという2枚のSVGにgrid-area: 1 / 1を付け忘れると、重なるはずの2枚が縦に並んでしまい、バッジ全体の高さが2倍になりました。

--iはインラインで振る

2つ目は03の3件スタックです。カードごとに変わるはずの--iをSCSS側に固定値で書いてしまうと、3枚とも同じオフセットを受け取って完全に重なって見えます。要素ごとにstyle="--i:0"のようにインラインで振らないと、デッキが開いて見える形になりません。

既定のUAスタイルは中央固定

3つ目は02のPopover APIトーストです。popover属性の既定UAスタイルは画面中央に固定するもので、position・top・right・marginをすべて上書きしないと角ではなく中央に浮きます。Popover APIはChrome・Edge 114以降、Safari 17以降、Firefox 125以降で使え(caniuseの対応表)、それより古いブラウザではpopover属性自体が無視されて普通の<div>のまま残ります。08で使ったenv(safe-area-inset-bottom)も同様にブラウザ依存で、iOS Safari 11.2以降でしか実際のノッチ値を返しません(caniuseのenv()対応表)。対応しないブラウザではcalc()のフォールバック値0pxが黙って適用されるだけなので、そちらは安全側に倒れます。

setPointerCaptureがないと途切れる

描画の途中に通知が割り込む事故は、ヒーローセクションのアニメーションのように最初の演出が長いページでとくに起きやすいものです。

4つ目は06のスワイプで閉じるです。pointerdownでsetPointerCaptureを呼んでいないと、指がカードの外に出た瞬間pointermoveが届かなくなり、ドラッグがそこで止まってしまいました。ここまで直した9本のファイルを開くパスワードはvp67sjt7で、下のカードから取り出せます。

アクセシビリティ

politeとassertiveの境目

9つとも実装コードにrole="status" aria-live="polite"を持たせています。読み上げソフトには伝えつつ、今の作業を止めさせない強さだからです。prefers-reduced-motion: reduceでは01・02・05・06・08はtransform: noneやopacity: 1に固定して登場・退場のアニメーションだけを止め、03は-22px・opacity: 1という開いた状態のままtransitionを切ります。07は両レイヤーのアニメーションを止め、状態色レイヤーだけを最終状態に固定し、09はベルの回転とバッジの拡大縮小をどちらも止めます。04だけは進行バーのアニメーションを残し、トーストが震える動きだけを取り除きます。進行バーは「いつ閉じるか」を伝える機能的な情報で、装飾ではないからです。5秒という自動で閉じるまでの時間はWCAG 2.2.1 Timing Adjustableが延長・解除の手段を求めており、05の「元に戻す」ボタンがそのままタイマーを止める役割を兼ねています。

次に読む

018のマイクロインタラクション9選にあるトーストの積み重ねは新着が来るたびに既存の分を押し上げるキューで、ここ03の3件スタックは常に3枚が固定されたデッキがhoverで開く点が逆方向です。003のモーダルアニメーション9選にあるボトムシートはハンドル付きの大きなパネルですが、08のモバイル下部トーストはハンドルのない一行のピル型で自動的に閉じます。ほかのCSSアニメーションはカテゴリー一覧に、このサイトの狙いはサイト紹介にまとめてあります。

FAQ

role="status"とrole="alert"、どちらを使うべきですか?

ほとんどのトーストはrole="status" aria-live="polite"が適切です。role="alert"やaria-live="assertive"は読み上げを即座に割り込ませる強さなので、決済失敗やセッション切れのように必ず今知らせたい通知だけに使います。

「トースト 通知 デザイン」で自動で閉じるまで何秒が目安ですか?

決まった正解はありませんが、WCAG 2.2.1は時間制限を延長・解除できる手段を求めています。04は5秒を基準にしつつ、05のように「元に戻す」ボタンでタイマーを止められる仕組みを添えて、読み終わる前に消える事態を避けています。

Popover APIに対応していないブラウザではどうなりますか?

popover属性が使えない環境では要素はただの<div>のまま残るので、'showPopover' in document.createElement('div')で対応状況を確認し、非対応のときだけクラスの付け外しで開閉するJSを別途用意します。通知が何枚も積み上がるときの順序と押し出しはトースト通知UIのスタックで別に扱っています。

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

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