CSS バリデーション アニメーション9選 — フォーム入力エラーの見せ方
CSS バリデーションのアニメーションは、揺れではなく色・アイコン・グローのような別の合図で、フォームの値が合っているか間違っているかを伝える動きです。以下9つはコピペで動く形にまとめ、会員登録フォームで実際に起きる順番に並べました。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 リアルタイムのインラインエラー
- 02 ラベルの状態色クロスフェード
- 03 フィールド検証のグローリング
- 04 成功バッジのバウンスチェック
- 05 エラーフィールドの呼吸リング
- 06 ステータスアイコンのスピナー→チェック・クロス変化
- 07 3段階パスワード強度バー
- 08 送信ボタンのローディング→成功変化
- 09 モーション削減時は静止表示に
9つは人気順ではなく、入力フォーム エラー表示をフォームを入力する人が実際に体験する順番で並べています。入力中にリアルタイムで出るメッセージ(01)からニックネームの使用可否(02)へ、検証を通過したフィールドが浮き上がる合図(03)と保存完了バッジ(04)を経て、消えずに残るエラー(05)と確認中の状態アイコン(06)、パスワード強度(07)、送信ボタンの一連の流れ(08)、最後はモーションを減らした人にも合図が残るか(09)という順です。
01リアルタイムのインラインエラー
html css バリデーションで何が見えるか
メールアドレス欄に不正な形式の値を入れると、その下にあらかじめ確保した行がわずかに滑り込んでから、また消えます。
コードのどこを見るか
使っているのは opacity と translateY だけなので、メッセージが現れている間も入力欄の高さ自体は変わりません。
.fv1__msg {
opacity: 0;
transform: translateY(-6px);
}
@keyframes fv1-in {
0%, 6% { opacity: 0; transform: translateY(-6px); }
28%, 78% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-6px); }
}
.fv1.is-demo .fv1__msg { animation: fv1-in $duration $easing infinite; }
02ラベルの状態色クロスフェード
画面で起きること
ニックネーム欄の上のラベルが、グレーの「ニックネーム」からミント色の「使用できるニックネームです」に変わります。
コードの読み方
color そのものを遷移させているのではなく、同じ位置に重ねた2枚のラベルが opacity で入れ替わっています。
@keyframes fv2-ok {
0%, 20% { opacity: 0; }
40%, 90% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes fv2-neutral {
0%, 20% { opacity: .6; }
40%, 90% { opacity: 0; }
100% { opacity: .6; }
}
.fv2.is-demo .fv2__label--ok { animation: fv2-ok $duration $easing infinite; }
.fv2.is-demo .fv2__label--neutral { animation: fv2-neutral $duration $easing infinite; }
03フィールド検証のグローリング
どんな動きか
検証に通ったフィールドの背後に、柔らかいミント色のリングが浮き上がります。
どこを変えるか
入力欄の裏に重ねた box-shadow レイヤーが opacity 0→1 と scale(.94→1.04) を組み合わせ、ふわっと膨らむような印象を作っています。
.fv3__glow {
position: absolute;
inset: -6px;
border-radius: $r-control;
box-shadow: 0 0 0 4px rgba(76, 212, 166, .55);
opacity: 0;
transform: scale(.94);
}
@keyframes fv3-glow {
0%, 15% { opacity: 0; transform: scale(.94); }
45% { opacity: 1; transform: scale(1.04); }
75%, 100% { opacity: 0; transform: scale(.94); }
}
.fv3.is-demo .fv3__glow { animation: fv3-glow $duration $easing infinite; }
04成功バッジのバウンスチェック
何が見えるか
保存が終わると円形バッジが弾むように現れ、その中のチェックが線を引くように完成します。
コードのどこを見るか
バッジは scale(.4→1.12→1) でバウンスし、チェックはSVGの polyline 一本の stroke-dashoffset を24から0まで減らして描いています。
@keyframes fv4-badge {
0%, 10% { opacity: 0; transform: scale(.4); }
35% { opacity: 1; transform: scale(1.12); }
50% { transform: scale(1); }
85%, 100% { opacity: 1; transform: scale(1); }
}
@keyframes fv4-draw {
0%, 30% { stroke-dashoffset: 24; }
55%, 100% { stroke-dashoffset: 0; }
}
.fv4__path { stroke-dasharray: 24; stroke-dashoffset: 24; }
.fv4.is-demo .fv4__badge { animation: fv4-badge $duration $easing infinite; }
.fv4.is-demo .fv4__path { animation: fv4-draw $duration linear infinite; }
05エラーフィールドの呼吸リング
画面で起きること
シェイクと違い、このフィールド自体は一切動きません。
コードの読み方
周りの淡いコーラル色のリングが opacity と scale(.97↔1.03) の間をゆっくり呼吸するように往復し、直っていないエラーを静かに伝え続けます。
.fv5__ring {
position: absolute;
inset: -5px;
border-radius: $r-control;
box-shadow: 0 0 0 3px $error-soft;
opacity: 0;
transform: scale(.97);
}
@keyframes fv5-breathe {
0%, 100% { opacity: 0; transform: scale(.97); }
50% { opacity: 1; transform: scale(1.03); }
}
.fv5.is-demo .fv5__ring { animation: fv5-breathe $duration $easing infinite; }
06ステータスアイコンのスピナー→チェック・クロス変化
どんな動きか
ユーザー名の重複確認中、フィールド右側のアイコンがスピナーで回った後、ミント色のチェックへ、続いてコーラル色のクロスへ引き継がれます。
どこを変えるか
3つのアイコンを同じ位置に重ねて opacity でクロスフェードさせ、rotate はスピナーだけに掛けています。
@keyframes fv6-spinner-show { 0%, 32% { opacity: 1; } 38%, 100% { opacity: 0; } }
@keyframes fv6-check-show { 0%, 32% { opacity: 0; } 40%, 62% { opacity: 1; } 70%, 100% { opacity: 0; } }
@keyframes fv6-cross-show { 0%, 62% { opacity: 0; } 70%, 96% { opacity: 1; } 100% { opacity: 0; } }
.fv6.is-demo .fv6__spinner {
animation: fv6-spin $dur-base linear infinite,
fv6-spinner-show $duration $easing infinite;
}
.fv6.is-demo .fv6__check { animation: fv6-check-show $duration $easing infinite; }
.fv6.is-demo .fv6__cross { animation: fv6-cross-show $duration $easing infinite; }
073段階パスワード強度バー
何が見えるか
同じトラックの上に、弱・中・強の3本のバーが重なっています。
コードのどこを見るか
それぞれ scaleX(transform-origin: left)で伸びたあと opacity で次の段階に引き継がれるため、色がじわじわ変わるのではなくバーそのものが差し替わって見えます。
.fv7__fill {
position: absolute; left: 0; top: 0; bottom: 0; width: 100%;
transform-origin: left center;
transform: scaleX(0);
opacity: 0;
}
@keyframes fv7-strong {
0%, 68% { transform: scaleX(.64); opacity: 0; }
78%, 96% { transform: scaleX(1); opacity: 1; }
100% { opacity: 0; transform: scaleX(0); }
}
.fv7.is-demo .fv7__fill--strong { animation: fv7-strong $duration $easing infinite; }
08送信ボタンのローディング→成功変化
画面で起きること
1つのボタンが、押される動き(scale(1→.96→1))→ローディングスピナー→ミント色のチェックの順に、1つのループの中で切り替わっていきます。
コードの読み方
ラベルが opacity で消えた場所でスピナーが rotate で回り、opacity・scale のクロスフェードでチェックへ引き継がれます。
@keyframes fv8-press { 0%, 8% { transform: scale(1); } 14% { transform: scale(.96); } 22%, 100% { transform: scale(1); } }
@keyframes fv8-check-show {
0%, 66% { opacity: 0; transform: scale(.5); }
74% { opacity: 1; transform: scale(1.15); }
86%, 100% { opacity: 1; transform: scale(1); }
}
.fv8.is-demo { animation: fv8-press $duration $easing infinite; }
.fv8.is-demo .fv8__check { animation: fv8-check-show $duration $easing infinite; }
09モーション削減時は静止表示に
どんな動きか
通常はステータスアイコンが scale(.5→1.15→1) と opacity でポップインしてから消えるループですが、モーションを減らす設定にすると、アニメーションが完全に切れ、チェックと文言は動かず最終状態のまま固定表示されます。
どこを変えるか
@keyframes fv9-pop {
0%, 10% { opacity: 0; transform: scale(.5); }
40% { opacity: 1; transform: scale(1.15); }
55%, 85% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(.5); }
}
.fv9.is-demo .fv9__status { animation: fv9-pop $duration $easing infinite; }
@media (prefers-reduced-motion: reduce) {
.fv9.is-demo .fv9__status { animation: none !important; opacity: 1; transform: scale(1); }
}
どこで落とし穴にはまるか — クロスフェードの罠
02番を作っているときに実際に踏んだ罠です。中立ラベルと成功ラベルを opacity でクロスフェードさせるとき、同じキーフレーム1つを animation-direction: reverse で鏡のように逆再生すればコードが短くなって楽だと考えました。ところが reverse は進行率(0〜100%)を反転させるだけで、キーフレームが完全に対称でない限り、ループの中間あたりで両方のラベルの opacity が同時に1になる瞬間が生まれ、文字が重なって見えてしまいました。レンダリングしたフレームを1枚ずつ開いて初めて気づき、直した方法は reverse をやめて、opacity の値がすべての段で正確に反対になる2つ目の @keyframes(fv2-neutral)を新しく書くことでした。9つ全部のvanilla・Reactコードが入った圧縮ファイルのパスワードはmk7s3ujkで、02番フォルダのこの2つの @keyframes を並べて見ると違いがそのまま見えます。
アクセシビリティ
9つとも prefers-reduced-motion: reduce でアニメーションを切ります。ただし03・04・07・08は形の変化そのものが情報なので、アニメーションだけ切ると結果自体が見えなくなってしまいます。これらの項目は途中の動きを飛ばし、最終状態(グローが点いた状態、チェックが描き終わった状態、バーが満タンの状態、送信ボタンにチェックが出た状態)をそのまま表示します。MDNの prefers-reduced-motion のドキュメントによれば、この値はOSの設定をそのまま読み取るため、モーションを減らしても検証結果という情報自体は残す必要があります。transform と opacity だけを動かしているのは、web.devの高性能アニメーションガイドにもある通り、この2つはレイアウトの再計算を伴わないためです。
揺れでエラーを伝えるバージョンはCSS 揺れるエラーアニメーション9選、チェックボックス・ラジオボタンの検証表示はCSS カスタムチェックボックス9選にあります。
FAQ
css バリデーションチェックで色を直接遷移させず opacity クロスフェードを使うのはなぜですか?
color や border-color を直接遷移させると、ブラウザは毎フレーム中間色を計算する必要があり、色がCSS変数から来ている画面(ダークモードなど)では中間色がおかしく見えることがあります。最終的な色をすでに持つ要素を2つ重ねて opacity だけを動かせば、この計算自体が不要になり、色は最初のフレームから正しい値のままです。
チェックアイコンは stroke-dashoffset の代わりに棒2本の回転で描いてもいいですか?
描けますが、2本の棒が同じ頂点から始まっていないと、チェックが斜め一本の線に見えてしまう不具合がよくあります。04番のようにSVGの polyline を stroke-dasharray・stroke-dashoffset で描けば、この始点のずれ自体が起きません。
9つを1つのフォームに全部同時に使っても大丈夫ですか?
動きますが、05の呼吸リングと03のグローのように常時動くループを何個も並べると、画面がせわしなく見えます。02・06のように状態が変わったときだけ動くものは残しつつ、05のように常にループするものは1画面に1つ以下にとどめるのがおすすめです。