ステッパー
カート→配送→決済→完了など手順の進捗を示すインジケータ。次へ/戻るボタンで進められる。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- カート→配送→決済の順序固定の手続き
- 長いフォームを画面分割し残り工程を見せたい
- オンボーディングの進捗表示
- 避ける場面
- 順不同で埋められる設定画面
- 2ステップしかない単純な確認
- ステップ数が可変でラベル配列と揃わない転用
- 構成要素
- ステップ円
- 番号/完了チェック
- ラベル
- 接続線
- 案内文
- 戻る/次へボタン
- 持っている状態
- 通常
- ホバー
- 押下中
- 無効
- 成功
- キーボード
Tab で戻る/次へボタンへ・Enter/Space で進退。ステップ円自体は操作できない
- 支援技術
完了は✓・現在は枠と影で色以外でも示す。現在ステップに aria-current=step、案内文は role=status で進退を告知、両端の戻る/次へは disabled を同期。✓と番号は aria-hidden
- 動き
円の色・枠 0.25s、接続線 0.25s、ボタン 0.15s。進んだ方向を帯として伝える。reduced-motion では即時
デザインの要点
- 状態は current の数値1つ。render() が円・線・案内文・ボタンの disabled を全部その値から引き直すので、どこかだけ古い表示が残らない
- 完了は✓、現在は枠と影、未達は灰色と、3状態を色以外の形でも分ける。色だけの区別は色弱の人と印刷で消える
- 末尾で「次へ」を見た目だけ薄くせず disabled 属性を付ける。透明度 0.4 のボタンはキーボードでは押せてしまう
- 案内文は role=status にする。ステップが進んだことは円の色では読み上げに伝わらない
使いどころ
チェックアウト・申し込みフォーム・オンボーディングなど、決まった手順を順番に進める複数ステップの処理に向く。今何ステップ目で、あと何ステップ残っているかを常に見せられるため、長いフォームを1画面ずつに分割しても迷いにくくなる。逆に、手順の順番が固定でない(ユーザーが好きな項目から埋められる設定画面など)や、ステップ数が2つしかない単純な確認画面には、ステッパーを置く分だけ画面が重くなり過剰装飾になりやすい。このデモは4ステップ固定のUIパーツで、実際のフォームでは各ステップのnext/backボタンに実際のバリデーションや画面遷移のロジックを結び付ける必要があり、現状はcurrentの数値を進退させてrender()を呼ぶだけの見本にとどまっている。
仕組み
状態は変数currentが単一のソースで、render()を呼ぶたびにstepsとlinesの全要素を舐めてis-done/is-activeクラスを再計算する単純な設計。i < currentは完了(is-done)、i === currentは現在地(is-active)、それ以外は未達という3分岐で、丸番号(.stp-num)と完了チェック(.stp-check)はどちらもdisplay:none/blockのトグルで排他に切り替えている。線(.stp-line)もis-doneクラスの有無だけで色を変えており、円と線を合わせて『どこまで進んだか』を一本の視覚的な帯として見せている。next/backボタンはcurrentをインクリメント/デクリメントしてrender()を呼び直すだけで、ステップごとの入力内容の保持やバリデーションは一切持たない(このデモの範囲外)。backBtn・nextBtnはどちらもdisabled属性で両端を止めており、見た目の薄さは:disabledのCSSから来ている。render()は合わせて現在ステップにaria-current="step"を付け直す。
実装の注意
nextBtnを末尾(isLast)で見た目だけopacity:0.4・cursor:defaultにする実装をよく見かけるが、disabled属性が無いとボタン自体はクリック可能かつフォーカス可能なままで、マウスカーソルの見た目だけを頼りに『押せない』と判断させることになり誤解を招く。この見本はbackBtn.disabled = current === 0 と nextBtn.disabled = isLast のように実際のdisabled属性を同期させ、薄い見た目は:disabledセレクタに任せている(inline styleで透明度を書くと、後からdisabledを足したときに二重管理になる)。もう一つ、ステップ数が可変(手順によって3つだったり5つだったりする)UIに転用する場合、labels配列とsteps/linesのNodeListの要素数を必ず一致させる必要があり、ズレるとmsg.textContent = labels[current]がundefinedを表示する。stp-descのmin-height:36pxは短い1行のメッセージを想定した固定値なので、2行に折り返す長い文言に差し替えると高さが揃わずガクつく。
アクセシビリティ
現在ステップの伝達は、is-activeクラスによる見た目の変化(枠の色・box-shadow)に加えて、render()内で該当する.stp-stepにaria-current="step"を付け、他からは外す形で行っている。これによりスクリーンリーダーは『4つのうち現在は2番目の配送』のように読める。.stp-msgはrole="status" aria-live="polite"を持つ<p>要素で、next/backのクリックのたびにtextContentが変わると手順が進んだことと次の指示文がその場で読み上げられる(assertiveにしないのは、ボタンを押した本人の操作結果なので割り込みまでは要らないため)。円の中の✓と番号はaria-hiddenにして、ラベルが『✓1カート』のように読まれないようにしてある。nextBtnは末尾でdisabled属性を持つので、スクリーンリーダー利用者にも『押せないボタン』として正しく扱われる。ステップ数を増やすときは、labels配列・.stp-step・.stp-lineの3つを同時に増やすこと。
調整して使う
ステップ数を増減させる、円の中に数字ではなくアイコンを入れる、線を直線ではなく進捗バー的なグラデーションで塗るなどが主な調整点。横並びが窮屈なモバイル幅では、円と線の並びを縦に変えたり、ラベルを非表示にして『2/4』のようなテキストだけの簡易表示に切り替える構成もよく使われる。ステップ間をクリックで直接ジャンプできるようにする(すでに完了したステップへ戻れる)拡張も考えられるが、その場合はis-doneの円にもクリックイベントを足し、まだ到達していない未来のステップへは飛べないようガードするロジックが要る。バリデーションと連携する実装では、次へボタンを押した時点で入力エラーがあればcurrentを進めずにエラーメッセージをstp-desc相当の領域へ出す分岐を追加することになる。
コード(コピーして使えます)
<div class="stp-root">
<ol class="stp-track" aria-label="購入手続き">
<li class="stp-step is-done" data-index="0">
<div class="stp-circle" aria-hidden="true"><span class="stp-check">✓</span><span class="stp-num">1</span></div>
<span class="stp-label">カート</span>
</li>
<li class="stp-line" aria-hidden="true"></li>
<li class="stp-step is-active" data-index="1" aria-current="step">
<div class="stp-circle" aria-hidden="true"><span class="stp-check">✓</span><span class="stp-num">2</span></div>
<span class="stp-label">配送</span>
</li>
<li class="stp-line" aria-hidden="true"></li>
<li class="stp-step" data-index="2">
<div class="stp-circle" aria-hidden="true"><span class="stp-check">✓</span><span class="stp-num">3</span></div>
<span class="stp-label">決済</span>
</li>
<li class="stp-line" aria-hidden="true"></li>
<li class="stp-step" data-index="3">
<div class="stp-circle" aria-hidden="true"><span class="stp-check">✓</span><span class="stp-num">4</span></div>
<span class="stp-label">完了</span>
</li>
</ol>
<div class="stp-desc">
<p class="stp-msg" role="status" aria-live="polite">配送先を入力してください</p>
</div>
<div class="stp-actions">
<button class="stp-back" disabled>← 戻る</button>
<button class="stp-next">次へ →</button>
</div>
</div>