ステップ進捗(ステッパー)

完了・現在・未到達を色分けし前後に移動できるマルチステップUI。接続線とチェック表示で、購入手続きやオンボーディングに使えます。

#css#javascript#animation

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW:モバイルオーダーの手続きをステッパーで表示 -->
<div class="cafe">
  <div class="cafe__bar">
    <span class="cafe__logo">☕ MOON BREW</span>
    <span class="cafe__tag">MOBILE ORDER</span>
  </div>

  <div class="stepper" data-stepper>
    <ol class="steps">
      <li class="step is-done"><span class="step__dot"></span><span class="step__label">商品を選ぶ</span></li>
      <li class="step is-current"><span class="step__dot"></span><span class="step__label">受取店舗</span></li>
      <li class="step"><span class="step__dot"></span><span class="step__label">お支払い</span></li>
      <li class="step"><span class="step__dot"></span><span class="step__label">完了</span></li>
    </ol>

    <div class="stage">
      <div class="stage__pane is-active" data-pane="0"><h2>カートの確認</h2><p>琥珀キャラメルラテ ×1 / 自家製スコーン ×1。合計 ¥1,000(税込)。</p></div>
      <div class="stage__pane" data-pane="1"><h2>受取店舗を選ぶ</h2><p>MOON BREW 中目黒店。受取時間は 11:30 前後を予定しています。</p></div>
      <div class="stage__pane" data-pane="2"><h2>お支払い方法</h2><p>登録済みのクレジットカードで決済します。ポイント120ptが付与されます。</p></div>
      <div class="stage__pane" data-pane="3"><h2>ご注文ありがとうございます</h2><p>準備ができ次第、アプリに通知します。お気をつけてお越しください。</p></div>
    </div>

    <div class="stepper__nav">
      <button class="btn btn--ghost" data-prev>戻る</button>
      <button class="btn btn--solid" data-next>次へ進む</button>
    </div>
  </div>
</div>
CSS
/* MOON BREW カフェ テーマ */
:root{--cream:#f5ede1;--brown:#2b1d12;--amber:#c98a3b;--line:#e3d6c2;--muted:#7a6450}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;display:grid;place-items:center;padding:14px;
  font-family:"Hiragino Mincho ProN","Segoe UI",serif;
  background:var(--cream);color:var(--brown);
}
.cafe{width:min(480px,100%)}
.cafe__bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.cafe__logo{font-weight:700;letter-spacing:.06em}
.cafe__tag{font-size:.72rem;letter-spacing:.22em;color:var(--amber)}
/* ステップ:接続線とドットで進捗を表現 */
.steps{list-style:none;display:flex;margin:0 0 18px;padding:0}
.step{flex:1;position:relative;text-align:center;font-size:.74rem;color:var(--muted)}
.step::before{
  content:"";position:absolute;top:9px;left:-50%;width:100%;height:2px;background:var(--line);z-index:0;
}
.step:first-child::before{display:none}
.step__dot{
  position:relative;z-index:1;display:block;width:20px;height:20px;margin:0 auto 6px;
  border-radius:50%;background:#fffaf2;border:2px solid var(--line);transition:all .3s;
}
.step.is-done .step__dot{background:var(--amber);border-color:var(--amber)}
.step.is-done .step__dot::after{content:"✓";color:#2b1d12;font-size:.7rem;font-weight:700;position:absolute;inset:0;display:grid;place-items:center}
.step.is-done::before{background:var(--amber)}
.step.is-current .step__dot{border-color:var(--amber);box-shadow:0 0 0 4px rgba(201,138,59,.2)}
.step.is-current,.step.is-done{color:var(--brown);font-weight:700}
.stage{background:#fffaf2;border:1px solid var(--line);border-radius:14px;padding:18px 20px;min-height:96px}
.stage__pane{display:none;animation:fade .35s ease}
.stage__pane.is-active{display:block}
.stage__pane h2{margin:0 0 8px;font-size:1.05rem}
.stage__pane p{margin:0;font-size:.88rem;line-height:1.7;color:var(--muted)}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.stepper__nav{display:flex;justify-content:space-between;gap:10px;margin-top:14px}
.btn{appearance:none;border:none;cursor:pointer;font:inherit;font-weight:700;padding:10px 20px;border-radius:10px;transition:opacity .2s}
.btn--ghost{background:transparent;border:1px solid var(--line);color:var(--muted)}
.btn--solid{background:var(--amber);color:#2b1d12}
.btn:disabled{opacity:.4;cursor:not-allowed}
@media (prefers-reduced-motion:reduce){.step__dot,.stage__pane{transition:none;animation:none}}
JavaScript
// ステッパー:前後ボタンでステップを移動し、状態を色分け
const root = document.querySelector('[data-stepper]');
if (root) {
  const steps = [...root.querySelectorAll('.step')];
  const panes = [...root.querySelectorAll('.stage__pane')];
  const prevBtn = root.querySelector('[data-prev]');
  const nextBtn = root.querySelector('[data-next]');
  const last = steps.length - 1;
  let current = 1; // 初期は「受取店舗」

  // 現在地に応じて完了/現在/未到達を塗り分け
  const render = () => {
    steps.forEach((s, i) => {
      s.classList.toggle('is-done', i < current);
      s.classList.toggle('is-current', i === current);
    });
    panes.forEach((p, i) => p.classList.toggle('is-active', i === current));
    if (prevBtn) prevBtn.disabled = current === 0;
    if (nextBtn) nextBtn.textContent = current === last ? '完了' : '次へ進む';
  };

  nextBtn?.addEventListener('click', () => {
    if (current < last) { current += 1; render(); }
  });
  prevBtn?.addEventListener('click', () => {
    if (current > 0) { current -= 1; render(); }
  });

  render();
}

コード

HTML
<!-- ステッパー:マルチステップ手続きの進捗を可視化し、前後に移動 -->
<div class="stepper" data-stepper>
  <ol class="steps">
    <li class="step is-active" data-step>
      <span class="step__dot"><span class="step__num">1</span></span>
      <span class="step__label">カート</span>
    </li>
    <li class="step" data-step>
      <span class="step__dot"><span class="step__num">2</span></span>
      <span class="step__label">配送先</span>
    </li>
    <li class="step" data-step>
      <span class="step__dot"><span class="step__num">3</span></span>
      <span class="step__label">支払い</span>
    </li>
    <li class="step" data-step>
      <span class="step__dot"><span class="step__num">4</span></span>
      <span class="step__label">完了</span>
    </li>
  </ol>

  <div class="stepper__panel">
    <p class="stepper__text" id="panelText">商品をカートに追加しました。次へ進んで配送先を入力してください。</p>
  </div>

  <div class="stepper__nav">
    <button class="nav-btn nav-btn--ghost" data-prev disabled>戻る</button>
    <button class="nav-btn nav-btn--primary" data-next>次へ</button>
  </div>
</div>
CSS
:root{
  --bg:#f5f7fb;
  --card:#ffffff;
  --done:#10b981;
  --active:#6366f1;
  --idle:#cbd5e1;
  --text:#1e293b;
  --muted:#64748b;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:26px 16px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:
    radial-gradient(500px 260px at 0% 0%,#e7ecff,transparent),
    radial-gradient(460px 240px at 100% 100%,#e6fff4,transparent),
    var(--bg);
}
.stepper{
  width:min(460px,100%);
  background:var(--card);border-radius:18px;
  box-shadow:0 18px 44px -22px rgba(30,41,90,.3);
  padding:28px 26px 24px;
}
/* ステップ列。接続線は ::before で表現 */
.steps{
  list-style:none;margin:0 0 22px;padding:0;
  display:flex;justify-content:space-between;position:relative;
}
.steps::before{
  content:"";position:absolute;top:16px;left:8%;right:8%;height:3px;
  background:var(--idle);border-radius:3px;z-index:0;
}
.step{
  position:relative;z-index:1;flex:1;
  display:flex;flex-direction:column;align-items:center;gap:8px;
}
.step__dot{
  width:34px;height:34px;border-radius:50%;
  display:grid;place-items:center;
  background:var(--card);border:3px solid var(--idle);
  transition:border-color .3s,background .3s,transform .3s;
}
.step__num{font-weight:700;font-size:.85rem;color:var(--muted);transition:color .3s}
.step__label{font-size:.78rem;color:var(--muted);font-weight:600;transition:color .3s}
/* 状態:現在 */
.step.is-active .step__dot{border-color:var(--active);transform:scale(1.12);box-shadow:0 0 0 5px rgba(99,102,241,.15)}
.step.is-active .step__num{color:var(--active)}
.step.is-active .step__label{color:var(--active)}
/* 状態:完了 */
.step.is-done .step__dot{background:var(--done);border-color:var(--done)}
.step.is-done .step__num{color:#fff}
.step.is-done .step__num::after{content:"✓";font-size:.9rem}
.step.is-done .step__num{font-size:0} /* 数字を隠してチェックを見せる */

.stepper__panel{
  background:#f1f5f9;border-radius:12px;padding:18px;margin-bottom:18px;
  min-height:78px;display:flex;align-items:center;
}
.stepper__text{margin:0;color:var(--muted);line-height:1.6;font-size:.92rem;animation:fade .3s ease}
.stepper__nav{display:flex;gap:10px;justify-content:flex-end}
.nav-btn{
  font:inherit;font-weight:600;cursor:pointer;border:none;border-radius:11px;padding:11px 22px;
  transition:transform .15s,filter .2s,opacity .2s;
}
.nav-btn:active{transform:translateY(1px)}
.nav-btn--ghost{background:#e2e8f0;color:var(--text)}
.nav-btn--ghost:disabled{opacity:.45;cursor:not-allowed}
.nav-btn--primary{color:#fff;background:linear-gradient(135deg,var(--active),#8b5cf6)}
.nav-btn--primary:hover{filter:brightness(1.07)}
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  .step__dot,.stepper__text{transition:none;animation:none}
}
JavaScript
// マルチステップの進捗を管理(前へ/次へ)
const root = document.querySelector('[data-stepper]');
if (root) {
  const steps = [...root.querySelectorAll('[data-step]')];
  const prevBtn = root.querySelector('[data-prev]');
  const nextBtn = root.querySelector('[data-next]');
  const panel = root.querySelector('#panelText');
  let current = 0;

  // 各ステップの説明文
  const MESSAGES = [
    '商品をカートに追加しました。次へ進んで配送先を入力してください。',
    '配送先の住所を入力します。お届け日時の指定もこちらで行えます。',
    'お支払い方法を選択します。クレジットカード・各種決済に対応。',
    'ご注文ありがとうございます。確認メールをお送りしました。',
  ];

  const render = () => {
    steps.forEach((step, i) => {
      step.classList.toggle('is-active', i === current);
      step.classList.toggle('is-done', i < current);
    });
    if (panel) panel.textContent = MESSAGES[current] || '';

    // ボタン状態を更新
    if (prevBtn) prevBtn.disabled = current === 0;
    if (nextBtn) nextBtn.textContent = current === steps.length - 1 ? '最初へ' : '次へ';
  };

  nextBtn?.addEventListener('click', () => {
    // 最終ステップの次は先頭へループ
    current = current >= steps.length - 1 ? 0 : current + 1;
    render();
  });
  prevBtn?.addEventListener('click', () => {
    current = Math.max(0, current - 1);
    render();
  });

  render();
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ステップ進捗(ステッパー)」の効果を追加してください。

# 追加してほしい効果
ステップ進捗(ステッパー)(UIコンポーネント)
完了・現在・未到達を色分けし前後に移動できるマルチステップUI。接続線とチェック表示で、購入手続きやオンボーディングに使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ステッパー:マルチステップ手続きの進捗を可視化し、前後に移動 -->
<div class="stepper" data-stepper>
  <ol class="steps">
    <li class="step is-active" data-step>
      <span class="step__dot"><span class="step__num">1</span></span>
      <span class="step__label">カート</span>
    </li>
    <li class="step" data-step>
      <span class="step__dot"><span class="step__num">2</span></span>
      <span class="step__label">配送先</span>
    </li>
    <li class="step" data-step>
      <span class="step__dot"><span class="step__num">3</span></span>
      <span class="step__label">支払い</span>
    </li>
    <li class="step" data-step>
      <span class="step__dot"><span class="step__num">4</span></span>
      <span class="step__label">完了</span>
    </li>
  </ol>

  <div class="stepper__panel">
    <p class="stepper__text" id="panelText">商品をカートに追加しました。次へ進んで配送先を入力してください。</p>
  </div>

  <div class="stepper__nav">
    <button class="nav-btn nav-btn--ghost" data-prev disabled>戻る</button>
    <button class="nav-btn nav-btn--primary" data-next>次へ</button>
  </div>
</div>

【CSS】
:root{
  --bg:#f5f7fb;
  --card:#ffffff;
  --done:#10b981;
  --active:#6366f1;
  --idle:#cbd5e1;
  --text:#1e293b;
  --muted:#64748b;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:26px 16px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:
    radial-gradient(500px 260px at 0% 0%,#e7ecff,transparent),
    radial-gradient(460px 240px at 100% 100%,#e6fff4,transparent),
    var(--bg);
}
.stepper{
  width:min(460px,100%);
  background:var(--card);border-radius:18px;
  box-shadow:0 18px 44px -22px rgba(30,41,90,.3);
  padding:28px 26px 24px;
}
/* ステップ列。接続線は ::before で表現 */
.steps{
  list-style:none;margin:0 0 22px;padding:0;
  display:flex;justify-content:space-between;position:relative;
}
.steps::before{
  content:"";position:absolute;top:16px;left:8%;right:8%;height:3px;
  background:var(--idle);border-radius:3px;z-index:0;
}
.step{
  position:relative;z-index:1;flex:1;
  display:flex;flex-direction:column;align-items:center;gap:8px;
}
.step__dot{
  width:34px;height:34px;border-radius:50%;
  display:grid;place-items:center;
  background:var(--card);border:3px solid var(--idle);
  transition:border-color .3s,background .3s,transform .3s;
}
.step__num{font-weight:700;font-size:.85rem;color:var(--muted);transition:color .3s}
.step__label{font-size:.78rem;color:var(--muted);font-weight:600;transition:color .3s}
/* 状態:現在 */
.step.is-active .step__dot{border-color:var(--active);transform:scale(1.12);box-shadow:0 0 0 5px rgba(99,102,241,.15)}
.step.is-active .step__num{color:var(--active)}
.step.is-active .step__label{color:var(--active)}
/* 状態:完了 */
.step.is-done .step__dot{background:var(--done);border-color:var(--done)}
.step.is-done .step__num{color:#fff}
.step.is-done .step__num::after{content:"✓";font-size:.9rem}
.step.is-done .step__num{font-size:0} /* 数字を隠してチェックを見せる */

.stepper__panel{
  background:#f1f5f9;border-radius:12px;padding:18px;margin-bottom:18px;
  min-height:78px;display:flex;align-items:center;
}
.stepper__text{margin:0;color:var(--muted);line-height:1.6;font-size:.92rem;animation:fade .3s ease}
.stepper__nav{display:flex;gap:10px;justify-content:flex-end}
.nav-btn{
  font:inherit;font-weight:600;cursor:pointer;border:none;border-radius:11px;padding:11px 22px;
  transition:transform .15s,filter .2s,opacity .2s;
}
.nav-btn:active{transform:translateY(1px)}
.nav-btn--ghost{background:#e2e8f0;color:var(--text)}
.nav-btn--ghost:disabled{opacity:.45;cursor:not-allowed}
.nav-btn--primary{color:#fff;background:linear-gradient(135deg,var(--active),#8b5cf6)}
.nav-btn--primary:hover{filter:brightness(1.07)}
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  .step__dot,.stepper__text{transition:none;animation:none}
}

【JavaScript】
// マルチステップの進捗を管理(前へ/次へ)
const root = document.querySelector('[data-stepper]');
if (root) {
  const steps = [...root.querySelectorAll('[data-step]')];
  const prevBtn = root.querySelector('[data-prev]');
  const nextBtn = root.querySelector('[data-next]');
  const panel = root.querySelector('#panelText');
  let current = 0;

  // 各ステップの説明文
  const MESSAGES = [
    '商品をカートに追加しました。次へ進んで配送先を入力してください。',
    '配送先の住所を入力します。お届け日時の指定もこちらで行えます。',
    'お支払い方法を選択します。クレジットカード・各種決済に対応。',
    'ご注文ありがとうございます。確認メールをお送りしました。',
  ];

  const render = () => {
    steps.forEach((step, i) => {
      step.classList.toggle('is-active', i === current);
      step.classList.toggle('is-done', i < current);
    });
    if (panel) panel.textContent = MESSAGES[current] || '';

    // ボタン状態を更新
    if (prevBtn) prevBtn.disabled = current === 0;
    if (nextBtn) nextBtn.textContent = current === steps.length - 1 ? '最初へ' : '次へ';
  };

  nextBtn?.addEventListener('click', () => {
    // 最終ステップの次は先頭へループ
    current = current >= steps.length - 1 ? 0 : current + 1;
    render();
  });
  prevBtn?.addEventListener('click', () => {
    current = Math.max(0, current - 1);
    render();
  });

  render();
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。