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

マイクロインタラクション CSS 9選 — 一行でコピペOK

マイクロインタラクション css は、ボタンを押す・保存する・タブを切り替える一つの操作に、画面がひと呼吸だけ反応して見せる小さなアニメーションです。この記事では9種類を選び、transitionと@keyframesだけで実装しました。グリッドの9つはiframeの中で最初から動いていて、クリックでは止められません。

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

並び順は派手さの順ではなく、反応がどこで完結するかです。まずボタン自身の見た目だけで完結する01〜03(押し込み・モーフ・アイコンスワップ)、次にサーバーの応答を待たず画面を先に書き換える04〜06、最後に要素の外側、入力欄の枠やタブの下線、通知の並びまで動く07〜09という順です。04〜06はAnimista・uiverseのようなライブラリではあまり見ない組み合わせを狙ったアイデア型として選びました。 状態の切り替えを画面全体へ広げるスイッチはCSS ダークモード 切り替え アニメーション9選が担い、操作の脇に短い説明を浮かべる向きはCSS ツールチップ 吹き出しアニメーション9選です。CSSだけで完結させるため、04のJavaScriptはis-likedというクラス名を付け外しするだけで、色や数値そのものは一行も触れていません。

01ボタンが押し込まれる

どんな動きか

ボタンを押すとスケールが0.96まで縮み、指を離すとバネのように弾んで元の大きさへ戻ります。

ボタンが押し込まれる
新しいタブで開く

01 · ボタンが押し込まれる — 計測 累積面積 1.256% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scale(0.96)스프링 이징active 상태

マイクロインタラクション アニメーションの使いどころ

押した瞬間に影も薄くなるため、実際にクリックが受け付けられたという触覚に近い感覚を画面だけで伝えられます。送信ボタンやアイコンボタン、カード全体をクリック領域にした場合に向いています。

.btn {
  transform: scale(1);
  box-shadow: $shadow-raised;
  transition: transform $duration $easing, box-shadow $duration $easing;

  &:active {
    transform: scale(.96);
    box-shadow: $shadow-press;
  }
}

@keyframes press-loop {
  0%, 12%   { transform: scale(1); box-shadow: $shadow-raised; }
  22%       { transform: scale(.96); box-shadow: $shadow-press; }
  38%, 100% { transform: scale(1); box-shadow: $shadow-raised; }
}

02保存アイコンがチェックに変わる

何が起きるか

保存ボタンを押すとフロッピー形状のアイコンがopacityとscaleだけで姿を消し、入れ替わりにチェックの2本の線がscaleXで描かれて完了を伝えます。

保存チェックアニメーション
新しいタブで開く

02 · 保存チェックアニメーション — 計測 累積面積 0.583% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleX 획 긋기색상 전환0.3초

向いている場面

背景の丸いバッジも同時にポップして色でも完了を強調します。フォームの自動保存ボタンやメモアプリの保存状態表示に向いています。

.save__check-bar {
  height: 3px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
}

.save.is-saved .save__check-bar--short {
  animation: check-short $duration $easing forwards;
  animation-delay: $duration * .35;
}

@keyframes check-short {
  to { transform: rotate(45deg) scaleX(1); }
}

@keyframes disk-out {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(.5); }
}

03コピー完了フィードバック

動きの内容

コピーアイコンを押すと二重に重なった用紙のアイコンが縮んで消え、同じ場所にチェックの線が現れます。

コピー完了フィードバック
新しいタブで開く

03 · コピー完了フィードバック — 計測 累積面積 1.494% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

아이콘 스왑툴팁 페이드1.2초 유지

使う場面

同時にボタンの上へ小さなツールチップが浮かび上がり、1.2秒表示されてから上へフェードアウトします。コードブロックのコピーボタンやAPIキーのコピーボタンに向いています。

.copy.is-copied .copy__sheet--front {
  opacity: 0;
  transform: scale(.6);
}

.copy.is-copied .copy__tip {
  animation: tip-float 1.4s $easing forwards;
}

@keyframes tip-float {
  0%   { opacity: 0; transform: translate(-50%, 4px); }
  15%  { opacity: 1; transform: translate(-50%, -2px); }
  85%  { opacity: 1; transform: translate(-50%, -2px); }
  100% { opacity: 0; transform: translate(-50%, -10px); }
}

04楽観的いいね(即時+ロールバック)

どんな動きか

ハートを押すとサーバーの応答を待たずに即座に赤く塗りつぶされ、数字のカウントも1つ繰り上がります。

楽観的いいね(即時+ロールバック)
新しいタブで開く

04 · 楽観的いいね(即時+ロールバック) — 計測 累積面積 0.354% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

낙관적 업데이트롤백 애니메이션0.8초

使いどころ

このデモは失敗を仮定したループになっていて、赤くなってから0.8秒後にはハートも数字も元の状態へロールバックします。SNSのいいねボタンやブックマークトグルに向いています。

var b = document.querySelector('.like');
b.addEventListener('click', function () {
  var liked = b.classList.toggle('is-liked');
  b.setAttribute('aria-pressed', liked);
  if (liked) {
    setTimeout(function () {
      b.classList.remove('is-liked');
      b.setAttribute('aria-pressed', 'false');
    }, 800);
  }
});

05スケルトン→本文クロスフェード

何が起きるか

グレーのスケルトンブロックと実際のテキスト・アバター画像が同じ位置に重なっていて、読み込みが終わるとopacityとscaleが入れ替わってクロスフェードします。

スケルトン→本文クロスフェード
新しいタブで開く

05 · スケルトン→本文クロスフェード — 計測 累積面積 3.401% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

opacity 크로스페이드겹친 레이어0.4초

向いている場面

位置がずれないため、読み込み完了の瞬間にレイアウトが跳ねません。カード一覧の読み込みやプロフィールページの初期表示に向いています。

.card__layer--skeleton {
  opacity: 1;
  transform: scale(1);
  transition: opacity $duration $easing, transform $duration $easing;
}

.card__layer--content {
  opacity: 0;
  transform: scale(.98);
  transition: opacity $duration $easing, transform $duration $easing;
}

.card.is-demo .card__layer--skeleton { animation: sk-fade $dur-loop $easing infinite; }
.card.is-demo .card__layer--content  { animation: content-fade $dur-loop $easing infinite; }

@keyframes content-fade {
  0%, 12%   { opacity: 0; transform: scale(.98); }
  55%, 100% { opacity: 1; transform: scale(1); }
}

06空状態アイコンのバウンス

動きの内容

検索結果や一覧が空のときに表示されるトレイのアイコンが、上下にゆっくりバウンスし続けます。

空状態アイコンのバウンス
新しいタブで開く

06 · 空状態アイコンのバウンス — 計測 累積面積 1.361% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateY 바운스ease-out 이징무한 반복

使う場面

動きが止まらないことで、この空白が読み込みエラーではなく意図された状態だと伝えます。検索結果なし画面や空のカート画面に向いています。

.empty__icon {
  transform: translateY(0);
}

.empty.is-demo .empty__icon {
  animation: bounce $duration $easing infinite;
}

@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-10px); }
}

07入力エラーで揺れる

どんな動きか

不正な値を送信すると入力欄の枠に赤いリングが重なって現れ、枠全体が左右に短く揺れます。

入力エラーで揺れる
新しいタブで開く

07 · 入力エラーで揺れる — 計測 累積面積 3.311% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateX 흔들림빨강 테두리0.4초

使いどころ

揺れの前後で速度を変えることで、機械的な単純往復ではなく実際に振動しているような手触りになります。ログインフォームのパスワード欄や決済フォームのカード番号入力に向いています。

.field__ring {
  border: 2.5px solid $error;
  opacity: 0;
}

.field.is-error .field__frame { animation: shake $duration $easing both; }
.field.is-error .field__ring  { opacity: 1; }

@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); }
}

08タブインジケータースライド

何が起きるか

タブをクリックするとカスタムプロパティ--iにインデックスが書き込まれ、下線のインジケーターがtranslateXで前のタブから次のタブの位置まで滑ります。

タブインジケータースライド
新しいタブで開く

08 · タブインジケータースライド — 計測 累積面積 0.5% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

transform 슬라이드width 보간0.25초

向いている場面

MDNのCSSカスタムプロパティの解説にあるとおりモダンブラウザ全般で使える仕組みですが、対応していない古いブラウザでは--iが反映されず、インジケーターは常に最初のタブの位置のまま動きません。設定ページのタブやダッシュボードのカテゴリタブに向いています。

.tabs {
  --i: 0;
}

.tabs__indicator {
  width: calc((100% - 8px) / 3);
  transform: translateX(calc(var(--i, 0) * 100%));
  transition: transform $duration $easing;
}

09トーストの積み重ね

動きの内容

新しいトーストが出るたびに、既存のトーストが1段ずつ上へ押し出されながらわずかに縮み、opacityも少しずつ下がります。

トーストの積み重ね
新しいタブで開く

09 · トーストの積み重ね — 計測 累積面積 19.122% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateY 밀림scale 축소순차 스택

使う場面

一番新しいトーストだけ背景色を変えているため、複数の通知が重なっても今どれが最新かひと目でわかります。保存完了通知や複数ファイルアップロードの進捗通知に向いています。

.toast {
  opacity: 0;
  transform: translateY(16px);
}
.toast--3 { background: $color; color: $subject-cream; }

@keyframes slot-3 {
  0%, 70%   { opacity: 0; transform: translateY(16px) scale(1); }
  80%, 92%  { opacity: 1; transform: translateY(0) scale(1); }
  98%, 100% { opacity: 0; transform: translateY(0) scale(1); }
}

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

連打でロールバックが二重に走る

9つとも状態の変化をtransformとopacityだけで表していて、位置やサイズの数値を直接書き換えるコードは使っていません。1つ目の落とし穴は04を組み立てているときに踏んだもので、ハートを連打すると0.8秒後のロールバックが二重に走り、赤色と数字が一瞬でちらついて見えました。setTimeoutを呼ぶ前に既存のタイマーをclearTimeoutで消してから、この二重発火は直りました。 押し込みやチェックに質感まで足したくなったら、スキューモーフィズムデザインの質感パーツ9選が01のスケールを触れる部品へ広げます。

タブ幅がそろわない

2つ目は08のタブを作ったときのことです。3つのタブの文字数を揃えずに試すと、それぞれの幅が微妙にずれてcalc((100% - 8px) / 3)で計算した位置とインジケーターの実際の位置が合わなくなり、下線がタブの真下からはみ出して止まって見えました。grid-template-columnsで3列を強制的に同じ幅にしてから、この崩れは直りました。修正済みの9ファイルを開くには dawuywyc という文字列が必要です。下のカードからzipを取ってください。

コンテナの高さが伸び縮みする

3つ目は09のトーストを組み立てたときのことです。リストの高さを固定していない状態で試すと、新しいトーストが増えるたびにコンテナ自体の高さが伸び縮みし、下にある要素がそのたびに位置を変えてがたつきました。heightを固定してからは、コンテナの外側は動かず内側のトーストだけが積み重なるようになりました。

アクセシビリティ

動きを減らす設定のとき

9つとも prefers-reduced-motion: reduce で動きが止まりますが、要素そのものは消えません。01のボタンは影も含めて:active時の見た目のまま止まり、04のハートと数字は最終的な色と値を保った状態でロールバックのアニメーションだけを省略します。08のタブインジケーターはtransitionだけを切り、クリックした瞬間に新しい位置へ即座に移動します。

@media (prefers-reduced-motion: reduce) {
  .btn, .save__ring, .save__disk, .save__check-bar,
  .copy__ring, .copy__sheet, .copy__check-bar, .copy__tip,
  .like__heart--fill, .like__count-track,
  .card__layer--skeleton, .card__layer--content,
  .empty__icon, .field__frame, .field__ring, .field__msg,
  .tabs__indicator, .toast {
    transition: none !important;
    animation: none !important;
  }
}

他の マイクロインタラクション 事例やCSSアニメーションはこちらのカテゴリー一覧に、このサイトの成り立ちは紹介ページにまとめています。

FAQ

マイクロインタラクション サンプルの動きはモバイルのタップでも同じように出ますか?

はい、01の:active擬似クラスと04・07・08のJavaScriptイベントは、どちらもタッチデバイスのpointerdownやclickでも同じように発火します。ただしiOSのSafariでは:activeがタップ直後の一瞬しか適用されないことがあり(MDN の :active の解説にタッチ端末での挙動の注記があります)、その場合は04や08のようにpointerdownでクラスを付け外しする方式のほうが安定して動きます。zipのvanilla/フォルダの各index.htmlは、この違いを踏まえてボタン系(01・02・03・04)にクリックイベントを使っています。

9個の状態はReactではどう管理しますか?

zipのreact/フォルダの各コンポーネントは、これまでと同じクラス名をdiv要素やbutton要素に付けているだけで、動きの速さと曲線はCSSのanimationとtransitionが担います。04のいいねボタンと07の入力エラー、09のトースト一覧だけはuseStateで状態(liked・error・トースト配列)を持ち、そのstateに応じてis-likedやis-errorに相当するクラス名を付け外しします。08のタブは選択中のインデックスをstateに持ち、インラインスタイルではなく--iカスタムプロパティへ渡しています。 クリックに応える定番の部品であるチェックボックスとラジオだけを集めたラジオボタン チェックボックス css カスタムアニメーション9選も、同じ原則でまとめてあります。

04の「先に画面を更新してあとで戻す」という考え方はどんな場面に向いていますか?

04の考え方は、サーバーの応答を待つ間もユーザーの操作を止めたくない場面に向いています。いいねやブックマークのように、失敗しても押し直せば済むうえ見た目の被害が小さい操作に向いていて、送金や削除のように取り返しがつかない操作には使うべきではありません。今回のデモは失敗を仮定したロールバックのみを再現していますが、実際のアプリでは成功した場合にサーバーからの返り値で数字を上書きする処理が別途必要です。

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

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