GODRICH

CSS 揺れる アニメーション 9選 — フォームのエラー表示

フォームでパスワードを間違えて送信すると、入力欄が左右にぷるぷる揺れて枠が赤く光る場面を見たことはないでしょうか。これがCSS 揺れる アニメーションで、JavaScriptを足さずCSSのキーフレームだけで動きます。

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

9つは人気順ではなく、フォームのどこでエラーが起きるかをたどる順番で並べています。値そのものが引っかかる入力欄(01〜03)から、要素は動かさず枠だけで知らせる控えめな合図(04)、パスワードのように重い失敗が続く場面(05〜06)へ進み、ブラウザが自分で処理してくれる場合(07)と同じ失敗が繰り返される場合(08)を経て、最後はモーションを減らした人にも合図が消えない工夫(09)で締めくくります。

01左右に揺れる

メール欄に間違った形式の値を入れてほかの場所をクリックすると、入力欄全体が左右に短く揺れてから止まり、枠が赤くなります。translateXの値を-7px→7px→-5px→3px→0pxの順に並べた@keyframesがひとつあるだけで、揺れ幅が少しずつ小さくなっていくので本物の震えのように見えます。

translateX@keyframes500ms
@keyframes sh-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
  18% { transform: translateX(-1px); }
  20% { transform: translateX(1px); }
}
.sh__input.sh--invalid { animation: sh-shake $duration $easing; }

02ぐらぐら回転

利用規約のチェックボックスを付けずに送信すると、チェックボックスとラベルをまとめた行全体が左右にゆらゆら傾きます。動いているのはrotateの角度だけで、チェックボックス自体の見た目には触れていません。

rotatetransform-origin600ms
@keyframes wg-wiggle {
  0%, 100% { transform: rotate(0); }
  15% { transform: rotate(-4deg); }
  30% { transform: rotate(4deg); }
  45% { transform: rotate(-3deg); }
  60% { transform: rotate(3deg); }
  75% { transform: rotate(-1.5deg); }
  90% { transform: rotate(1.5deg); }
}
.wg__row.wg--wiggle { animation: wg-wiggle $duration $easing; }

03ぷるぷる

保存ボタンを押して失敗すると、ボタンがゼリーのようにぷるぷる揺れてから収まります。skewXskewYを同じキーフレームの中で一緒にねじっているため、斜め2方向に同時につぶれるように見えるのがコツです。

skewXskewY800ms
@keyframes je-jello {
  0%, 100% { transform: none; }
  15% { transform: skewX(-9deg) skewY(-9deg); }
  30% { transform: skewX(7deg) skewY(7deg); }
  45% { transform: skewX(-5deg) skewY(-5deg); }
  60% { transform: skewX(3deg) skewY(3deg); }
  75% { transform: skewX(-2deg) skewY(-2deg); }
  90% { transform: skewX(1deg) skewY(1deg); }
}
.je__btn.je--jello { animation: je-jello $duration $easing; }

04エラー枠フラッシュ

カード番号欄そのものは動かず、背後に重ねた赤い枠のレイヤーだけが2回パッと光って消えます。入力欄自体ではなく専用のspanopacityだけで点滅させているので、隣に並ぶほかの欄まで揺れて見える心配がありません。

opacityinset: 0500ms
.bf__ring {
  position: absolute;
  inset: -$sp-1;
  border: 2px solid $color;
  border-radius: $r-sm;
  opacity: 0;
}
@keyframes bf-flash {
  0%, 100% { opacity: 0; }
  15% { opacity: 1; }
  30% { opacity: 0; }
  45% { opacity: 1; }
  60% { opacity: 0; }
}
.bf__ring.bf--flash { animation: bf-flash $duration $easing; }

05入力欄ヘッドシェイク

パスワード確認欄が一致しないと、入力欄が首を横に振るように少しだけ立体的に揺れます。親要素にperspectiveを与えたうえでtranslateXrotateYを同じキーフレームで組み合わせているのが仕掛けで、perspectiveがないとこの立体感は出ません。

rotateYperspective900ms
.hs { perspective: 500px; }
@keyframes hs-shake {
  0% { transform: translateX(0); }
  6.5% { transform: translateX(-6px) rotateY(-9deg); }
  18.5% { transform: translateX(5px) rotateY(7deg); }
  31.5% { transform: translateX(-3px) rotateY(-5deg); }
  43.5% { transform: translateX(2px) rotateY(3deg); }
  50%, 100% { transform: translateX(0); }
}
.hs__input.hs--shake { animation: hs-shake $duration $easing; }

06警告アイコンバウンス

赤い丸に「!」が入ったアイコンが、エラーが出た瞬間にぴょんと跳ね上がって着地し、視線をエラーメッセージへ誘導します。scaletranslateYを同じキーフレームで動かし、ばねのように行き過ぎてから収まるcubic-bezierのイージングを使います。アイコンはaria-hiddenで読み上げから外し、意味は横の文章が伝えます。

scaletranslateY700ms
@keyframes ib-bounce {
  0%, 100% { transform: translateY(0) scale(1); }
  20% { transform: translateY(-10px) scale(1.15); }
  40% { transform: translateY(0) scale(.95); }
  55% { transform: translateY(-4px) scale(1.05); }
  70% { transform: translateY(0) scale(1); }
}
.ib__icon.ib--bounce { animation: ib-bounce $duration $easing; }

07:user-invalidで自動的に揺れる

メール欄に値を入れてからほかの場所へフォーカスを移すと、JavaScriptを1行も書かなくてもブラウザ自身が揺れを再生します。効いているのはinput:user-invalidという疑似クラスだけで、ページを開いた直後や何も触っていない空欄にはこの状態がつきません。

:user-invalidpattern500ms
.ui__input:user-invalid {
  border-color: $color;
  animation: ui-shake $duration $easing;
}
@keyframes ui-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
}

081回再生のリセットパターン

「再確認」ボタンを連打しても、毎回ちゃんと最初から揺れ直します。同じクラスを消してすぐ付け直すだけだとブラウザは「再生中のアニメーション」とみなして無視するため、消してからoffsetWidthを一度読んで強制的にリフローを起こし、そのあとで付け直しています。

animationendreflowJS 6줄
var input = document.getElementById('rs-pw');
document.getElementById('rs-check').addEventListener('pointerdown', function () {
  input.classList.remove('rs--shake');
  void input.offsetWidth;
  input.classList.add('rs--shake');
});

09モーション減設定時はアウトライン

普段は01と同じtranslateXで揺れますが、OSの設定で「動きを減らす」を選んでいる人には、揺れの代わりに赤い枠が2回ふわっと点滅する合図に置き換わります。動きを消しても、エラーが起きたという合図まで一緒に消えてしまわないようにするための分岐です。

prefers-reduced-motionoutlineopacity
@media (prefers-reduced-motion: reduce) {
  .rm__textarea.rm--invalid,
  .rm.is-demo .rm__textarea { animation: none !important; }
  .rm__ring.rm--invalid { animation: rm-ring-pulse $dur-loop ease-in-out; }
}
@keyframes rm-ring-pulse {
  0%, 100% { opacity: 0; }
  25% { opacity: 1; }
  50% { opacity: 0; }
  75% { opacity: 1; }
}

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

JSでクラスを付け外しする項目は、CSSアニメーションが一度再生し終わると、同じ値でまた失敗してもクラスが付いたままなら反応しないという共通の落とし穴を抱えています。08を作る前、同じクラスをremove()せずにもう一度add()しただけで試したところ、document.getAnimations()で数えたアニメーションの数が変わらず、2回目の失敗では入力欄がまったく揺れませんでした。08のコードにある「消す→offsetWidthを読む→付け直す」という3手順は、ほかの項目にもそのまま流用できる直し方です。解凍パスワードは3v34eb5kで、下のzipカードから受け取れます。

アクセシビリティ

9つともprefers-reduced-motion: reduceではanimation: none !importantで通常の揺れを止めます。ただし04と09は揺れや点滅そのものがエラーの合図を兼ねているため、04は文章の色と言葉だけで合図を残し、09だけはtranslateXの揺れを消す代わりにopacityで点滅する赤いリングを別に用意して、動きを減らした人にも合図が消えないようにしています。:user-invalidのような検証系の疑似クラスはMDNのドキュメントによるとChrome/Edge 119以降・Firefox 88以降・Safari 16.4以降で動き、それより古いブラウザは静かに無視するだけなので、会員登録や決済のように見逃せないフォームでは01や05のようなクラス切り替え方式を基本にしておくのが安全です。

浮く・跳ねる・回るといった基本のCSSキーフレームをまとめて見るならCSS キーフレーム アニメーション9選へ、このサイトがほかに何を扱っているかはCSSのカテゴリー一覧からどうぞ。

FAQ

なぜtransformやopacityしか動かさないのですか?

marginleftを動かすとブラウザは毎フレームレイアウトを計算し直す必要があり、揺れている要素の横に並ぶほかの欄まで押し出されて見えることがあります。transformopacityはレイアウトの再計算なしにすでに描画されたピクセルを動かすだけなので、なめらかに動いて周りにも影響しません。

:invalid:user-invalidは何が違いますか?

:invalidは値が空だったり形式が違うだけでページを開いた瞬間から一致してしまうため、何も入力していない空欄までいきなり赤く光ることがあります。:user-invalidはユーザーが実際に値を触ってフォーカスを外したあとにだけ一致するので、まだ何も書いていない欄を開いた瞬間に揺らしてしまう誤作動を防げます。

同じ揺れをもう一度再生したいときはどうすればいいですか?

08の項目のように、クラスを一度remove()してoffsetWidthを読み、強制的にリフローを起こしてから同じクラスをadd()し直せば、ブラウザは新しいアニメーションとして最初から再生します。Reactではこの代わりに要素のkeyを変えて丸ごと再マウントするやり方のほうが素直です。

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

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