送信ボタン演出
送信ボタンを押すとスピナーが回り、完了後にチェックマークへ変化。処理中の状態を明示する。
プレビュー
操作クリック/タップで動きます。
使いどころ
送信後の状態遷移(送信中→完了→通常)をボタン自身の中だけで完結させたい場面に向く。フォーム送信・お気に入り登録・ワンクリック決済のように、処理が数秒以内に終わり、かつ完了したことをその場で確認させたい単発アクションが適する。実装は完了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できる形へ変える配慮が要る。また、テキストの変化(送信する/送信中…/完了 ✓)は視覚のみでaria-liveが無いため、スクリーンリーダーには一切変化が伝わらない点も見落としやすい。
アクセシビリティ
現状の実装はaria-live等の音声フィードバックを一切持たない。ボタン内のテキストはsub-textのtextContentを書き換えるだけで、変化を検知させるにはボタンを包む親、またはページ内の別要素にaria-live="polite"のライブリージョンを用意し、状態が変わるたびに「送信中です」「送信が完了しました」のような短い文言を差し込む必要がある。disabled属性自体はネイティブのボタン無効化なのでキーボード・スクリーンリーダーの両方に伝わり、連打防止の面では機能している。スピナー部分のsub-iconにはaria-hiddenも役割も付いていないため、装飾なのか情報なのか支援技術には判断がつかない状態で、少なくともaria-hidden="true"を明示すべきである。prefers-reduced-motionの分岐も無く、回転アニメーションは常に動くため、spinning時のanimationを止める分岐を追加するのが望ましい。
調整して使う
調整点は主に3つ。まず各フェーズの表示時間(送信中1800ms・完了表示2000ms)で、実処理にかかる時間が読めるならその実測に合わせ、完了表示は「見せて安心させる」ために最低でも1秒は残すのが目安になる。次に完了後の挙動で、現状は自動的に元の「送信する」へ戻るが、遷移先ページへ移動する処理(会員登録など)ではリセットせずdone状態のまま画面遷移させた方が自然な場合が多い。最後に色とアイコンで、成功を緑にする既定に加えて、エラー時のバリエーション(赤背景+×アイコン+「失敗しました」+disabled解除)を同じ構造で用意しておくと、通信エラー時にも同じコンポーネントで表現を完結させられる。二段階(送信中→完了)ではなく三段階目としてエラーを設計に組み込むのが実用上のポイントになる。
コード(コピーして使えます)
<button class="submit-btn" id="sub">
<span class="sub-icon" id="sub-icon"></span>
<span class="sub-text" id="sub-text">送信する</span>
</button>