送信ボタン演出
送信ボタンを押すとスピナーが回り、完了後にチェックマークへ変化。処理中の状態を明示する。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- 数秒で終わる送信・登録のボタン
- 送信中→完了をボタン内で完結させたい操作
- 連打を防ぎたい単発アクション
- 避ける場面
- 10 秒以上かかる処理(進捗が出ない)
- 失敗の表現が要る送信
- 完了後にページ遷移する登録(自動で戻る)
- 構成要素
- 送信ボタン
- スピナー
- チェックアイコン
- 状態ラベル
- 持っている状態
- 通常
- 無効
- 読み込み中
- 成功
- キーボード
Tab でボタンへ、Enter/Space で送信。送信中 1.8 秒と完了表示 2 秒の間は disabled
- 支援技術
disabled はネイティブで支援技術に伝わる。状態の文言(送信中…/完了 ✓)は aria-live=polite の span で読み上げ、スピナーは aria-hidden。幅固定でずれない
- 動き
スピナー 0.7s linear 回転、背景色 250ms。送信中 1.8s→完了 2s→リセット。reduced-motion で回転を止める
デザインの要点
- ボタンは min-width:150px で固定する。文言が「送信する→送信中…→完了 ✓」と変わっても幅が揺れない
- 押した瞬間に disabled にする。連打で二重送信になるのを防ぐのは、演出ではなくこの 1 行
- 完了表示は 2 秒残してから戻す。押した直後に元へ戻ると、送れたのか分からない
- 状態の文言は aria-live=polite の span に書く。見た目のスピナーだけでは読み上げに何も起きない
使いどころ
送信後の状態遷移(送信中→完了→通常)をボタン自身の中だけで完結させたい場面に向く。フォーム送信・お気に入り登録・ワンクリック決済のように、処理が数秒以内に終わり、かつ完了したことをその場で確認させたい単発アクションが適する。実装は完了2秒後に自動でボタン表示を元へ戻す設計なので、完了状態を確認してから別の操作に移ってほしい場面と相性がよい。逆に、処理に10秒以上かかる可能性がある送信(大きなファイルアップロード等)にはこのままでは向かない。進捗の割合を示す手段がなく、スピナーが回り続けるだけでは「本当に動いているのか」という不安を長時間与えてしまう。また処理が失敗した場合の見た目がこの実装には存在しないため、失敗を伝えるにはicon/textの分岐とボタン色をエラー用に追加する必要がある。
仕組み
btn.disabledを立てて連打を防ぎつつ、sub-icon要素のclassNameをsub-icon spinning→sub-icon check→sub-iconの順に切り替えるだけの単純なステートマシンで、実際の非同期処理の代わりに2つのsetTimeout(1800ms・2000ms)でタイミングを模している。スピナーはborder-top-colorだけを変えた円をtransform: rotate(360deg)でinfinite回転させるオーソドックスな作り、完了チェックはbackground-imageに埋め込んだdata URIのSVGをbefore疑似要素で表示する方式で、追加のHTTPリクエストが発生しない。ボタン自体はdoneクラスで背景色を緑に変えるが、min-width: 150pxで固定してあるため、テキストが「送信する」→「送信中…」→「完了 ✓」と変化しても幅が動かずレイアウトが揺れない工夫がされている。
実装の注意
この実装はsetTimeoutで処理完了を偽装しているだけなので、実際のfetch/Server Actionに差し替える際はcatchでエラー時の分岐(アイコンをエラー表示に変える・disabledを解除する)を追加し忘れやすい。現状は成功パスしか存在しない。次に、setTimeoutのIDをどこにも保持していないため、コンポーネントが破棄された後もタイマーは走り続け、既に無いDOMへtextContentを書き込もうとするリスクがSPA的な使い方では起こりうる。クリーンアップ時にclearTimeoutできる形へ変える配慮が要る。また、テキストの変化(送信する/送信中…/完了 ✓)はsub-textのaria-live=politeで読み上げに乗せているが、これをただのspanに戻すとスクリーンリーダーには一切変化が伝わらなくなる点も見落としやすい。
アクセシビリティ
音声フィードバックは、ボタン内のsub-text(状態の文言)にaria-live="polite"を付けることで実現している。textContentが「送信中…」「完了 ✓」と書き換わるたびにその文言が読み上げられ、ライブリージョンをボタンの外に別途用意しなくても状態変化が届く。disabled属性はネイティブのボタン無効化なのでキーボード・スクリーンリーダーの両方に伝わり、連打防止の面でも機能している。スピナー部分のsub-iconはaria-hidden="true"で装飾として明示し、回転する枠線が「画像」として読まれるのを防ぐ。prefers-reduced-motion: reduceではspinning時のanimationを止め、静止した部分リングと「送信中…」の文言だけで処理中を示す。フォーカスは:focus-visibleで2pxの輪郭を2px離して描く。
調整して使う
調整点は主に3つ。まず各フェーズの表示時間(送信中1800ms・完了表示2000ms)で、実処理にかかる時間が読めるならその実測に合わせ、完了表示は「見せて安心させる」ために最低でも1秒は残すのが目安になる。次に完了後の挙動で、現状は自動的に元の「送信する」へ戻るが、遷移先ページへ移動する処理(会員登録など)ではリセットせずdone状態のまま画面遷移させた方が自然な場合が多い。最後に色とアイコンで、成功を緑にする既定に加えて、エラー時のバリエーション(赤背景+×アイコン+「失敗しました」+disabled解除)を同じ構造で用意しておくと、通信エラー時にも同じコンポーネントで表現を完結させられる。二段階(送信中→完了)ではなく三段階目としてエラーを設計に組み込むのが実用上のポイントになる。
コード(コピーして使えます)
<button class="submit-btn" id="sub">
<span class="sub-icon" id="sub-icon" aria-hidden="true"></span>
<span class="sub-text" id="sub-text" aria-live="polite">送信する</span>
</button>