ステップインジケータ

1→2→3→4の水平ステップを完了・現在・未完で色分けし、接続線が進むごとに満ちていく進行状況の表示。次へ/戻るで手動操作もできます。

#css#js#stepper

ライブデモ

コード

HTML
<!-- ステップインジケータ: 4段階の進行を色分けと接続線の充填で表現し、次へ/戻るで操作できる -->
<div class="stage">
  <div class="wizard">
    <div class="wizard__steps" id="ssiSteps" role="progressbar" aria-valuemin="1" aria-valuemax="4" aria-valuenow="1" aria-valuetext="ステップ 1 / 4: アカウント">
      <ol class="steps" aria-hidden="true">
        <li class="step" data-step="1">
          <span class="step__circle">
            <span class="step__num">1</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">アカウント</span>
        </li>
        <li class="connector"><span class="connector__fill"></span></li>
        <li class="step" data-step="2">
          <span class="step__circle">
            <span class="step__num">2</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">お届け先</span>
        </li>
        <li class="connector"><span class="connector__fill"></span></li>
        <li class="step" data-step="3">
          <span class="step__circle">
            <span class="step__num">3</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">お支払い</span>
        </li>
        <li class="connector"><span class="connector__fill"></span></li>
        <li class="step" data-step="4">
          <span class="step__circle">
            <span class="step__num">4</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">確認</span>
        </li>
      </ol>
    </div>
    <div class="wizard__actions">
      <button type="button" class="btn btn--ghost" id="ssiBack">戻る</button>
      <button type="button" class="btn btn--primary" id="ssiNext">次へ</button>
    </div>
  </div>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 18% 18%, #1f2b3a 0%, transparent 50%),
    radial-gradient(circle at 85% 85%, #262038 0%, transparent 52%),
    #10131a;
  color: #eef1f6;
}

.stage {
  width: 100%;
  display: grid;
  place-items: center;
}

.wizard {
  width: min(420px, 100%);
  padding: 30px 24px 24px;
  border-radius: 20px;
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.015));
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4);
}

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: flex-start;
}

.step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  width: 64px;
}

.step__circle {
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.06);
  border: 2px solid rgba(255, 255, 255, 0.16);
  font-size: 13px;
  font-weight: 700;
  color: rgba(238, 241, 246, 0.55);
  transition: background-color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease, transform 0.3s ease;
}

.step__num,
.step__check {
  grid-area: 1 / 1;
  transition: opacity 0.25s ease, transform 0.25s ease;
}

.step__check {
  opacity: 0;
  transform: scale(0.5);
  color: #0c1a12;
  font-size: 15px;
}

.step__label {
  font-size: 11px;
  letter-spacing: 0.02em;
  color: rgba(238, 241, 246, 0.45);
  text-align: center;
  transition: color 0.4s ease;
}

.connector {
  position: relative;
  flex: 1 1 auto;
  height: 34px;
  display: flex;
  align-items: center;
  padding: 0 2px;
}

.connector::before {
  content: "";
  display: block;
  width: 100%;
  height: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
}

.connector__fill {
  position: absolute;
  left: 2px;
  right: 2px;
  top: 50%;
  height: 3px;
  width: 0;
  transform: translateY(-50%);
  border-radius: 999px;
  background: linear-gradient(90deg, #38bdf8, #34d399);
  transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

.connector.is-filled .connector__fill {
  width: calc(100% - 4px);
}

/* 現在のステップ */
.step.is-current .step__circle {
  background: rgba(56, 189, 248, 0.16);
  border-color: #38bdf8;
  color: #7dd3fc;
  box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.14);
  transform: scale(1.06);
}

.step.is-current .step__label {
  color: #eef1f6;
  font-weight: 600;
}

/* 完了したステップ */
.step.is-complete .step__circle {
  background: linear-gradient(140deg, #34d399, #22c07a);
  border-color: transparent;
}

.step.is-complete .step__num {
  opacity: 0;
  transform: scale(0.5);
}

.step.is-complete .step__check {
  opacity: 1;
  transform: scale(1);
}

.step.is-complete .step__label {
  color: rgba(238, 241, 246, 0.75);
}

.wizard__actions {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 26px;
}

.btn {
  flex: 1 1 auto;
  padding: 10px 16px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 12px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: transform 0.15s ease, background-color 0.2s ease, opacity 0.2s ease;
}

.btn:active {
  transform: scale(0.97);
}

.btn--ghost {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.14);
  color: rgba(238, 241, 246, 0.75);
}

.btn--ghost:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.btn--primary {
  background: linear-gradient(135deg, #38bdf8, #34d399);
  color: #0b1220;
}

@media (max-width: 380px) {
  .wizard { padding: 24px 16px 18px; }
  .step { width: 52px; }
}
JavaScript
// ステップインジケータ: 現在地を色分けし、接続線を充填する。自動進行とNext/Backの手動操作を両立させる
(() => {
  const root = document.getElementById('ssiSteps');
  const backBtn = document.getElementById('ssiBack');
  const nextBtn = document.getElementById('ssiNext');
  if (!root) return; // null安全

  const stepsEls = Array.from(root.querySelectorAll('.step'));
  const connectors = Array.from(root.querySelectorAll('.connector'));
  if (!stepsEls.length) return;

  const TOTAL = stepsEls.length;
  const LABELS = stepsEls.map((el) => el.querySelector('.step__label')?.textContent?.trim() || '');
  const AUTO_MS = 2400;
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let current = 1;
  let autoTimer = null;

  function render() {
    stepsEls.forEach((el, i) => {
      const n = i + 1;
      el.classList.toggle('is-complete', n < current);
      el.classList.toggle('is-current', n === current);
      el.classList.toggle('is-upcoming', n > current);
    });
    connectors.forEach((el, i) => {
      el.classList.toggle('is-filled', i + 1 < current);
    });

    root.setAttribute('aria-valuenow', String(current));
    root.setAttribute('aria-valuetext', `ステップ ${current} / ${TOTAL}: ${LABELS[current - 1]}`);

    if (backBtn) backBtn.disabled = current === 1;
    if (nextBtn) nextBtn.textContent = current === TOTAL ? '完了' : '次へ';
  }

  function goTo(n) {
    // 1〜TOTALの範囲でループさせる(完了の次はアカウントへ)
    current = ((n - 1 + TOTAL) % TOTAL) + 1;
    render();
  }

  function restartAuto() {
    if (reduce) return;
    clearInterval(autoTimer);
    autoTimer = setInterval(() => goTo(current + 1), AUTO_MS);
  }

  nextBtn?.addEventListener('click', () => {
    goTo(current + 1);
    restartAuto();
  });
  backBtn?.addEventListener('click', () => {
    goTo(current - 1);
    restartAuto();
  });

  render();
  restartAuto();
})();

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

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

# 追加してほしい効果
ステップインジケータ(カレンダー & ステップ)
1→2→3→4の水平ステップを完了・現在・未完で色分けし、接続線が進むごとに満ちていく進行状況の表示。次へ/戻るで手動操作もできます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ステップインジケータ: 4段階の進行を色分けと接続線の充填で表現し、次へ/戻るで操作できる -->
<div class="stage">
  <div class="wizard">
    <div class="wizard__steps" id="ssiSteps" role="progressbar" aria-valuemin="1" aria-valuemax="4" aria-valuenow="1" aria-valuetext="ステップ 1 / 4: アカウント">
      <ol class="steps" aria-hidden="true">
        <li class="step" data-step="1">
          <span class="step__circle">
            <span class="step__num">1</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">アカウント</span>
        </li>
        <li class="connector"><span class="connector__fill"></span></li>
        <li class="step" data-step="2">
          <span class="step__circle">
            <span class="step__num">2</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">お届け先</span>
        </li>
        <li class="connector"><span class="connector__fill"></span></li>
        <li class="step" data-step="3">
          <span class="step__circle">
            <span class="step__num">3</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">お支払い</span>
        </li>
        <li class="connector"><span class="connector__fill"></span></li>
        <li class="step" data-step="4">
          <span class="step__circle">
            <span class="step__num">4</span>
            <span class="step__check" aria-hidden="true">✓</span>
          </span>
          <span class="step__label">確認</span>
        </li>
      </ol>
    </div>
    <div class="wizard__actions">
      <button type="button" class="btn btn--ghost" id="ssiBack">戻る</button>
      <button type="button" class="btn btn--primary" id="ssiNext">次へ</button>
    </div>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 18% 18%, #1f2b3a 0%, transparent 50%),
    radial-gradient(circle at 85% 85%, #262038 0%, transparent 52%),
    #10131a;
  color: #eef1f6;
}

.stage {
  width: 100%;
  display: grid;
  place-items: center;
}

.wizard {
  width: min(420px, 100%);
  padding: 30px 24px 24px;
  border-radius: 20px;
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.015));
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4);
}

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: flex-start;
}

.step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  width: 64px;
}

.step__circle {
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.06);
  border: 2px solid rgba(255, 255, 255, 0.16);
  font-size: 13px;
  font-weight: 700;
  color: rgba(238, 241, 246, 0.55);
  transition: background-color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease, transform 0.3s ease;
}

.step__num,
.step__check {
  grid-area: 1 / 1;
  transition: opacity 0.25s ease, transform 0.25s ease;
}

.step__check {
  opacity: 0;
  transform: scale(0.5);
  color: #0c1a12;
  font-size: 15px;
}

.step__label {
  font-size: 11px;
  letter-spacing: 0.02em;
  color: rgba(238, 241, 246, 0.45);
  text-align: center;
  transition: color 0.4s ease;
}

.connector {
  position: relative;
  flex: 1 1 auto;
  height: 34px;
  display: flex;
  align-items: center;
  padding: 0 2px;
}

.connector::before {
  content: "";
  display: block;
  width: 100%;
  height: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
}

.connector__fill {
  position: absolute;
  left: 2px;
  right: 2px;
  top: 50%;
  height: 3px;
  width: 0;
  transform: translateY(-50%);
  border-radius: 999px;
  background: linear-gradient(90deg, #38bdf8, #34d399);
  transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

.connector.is-filled .connector__fill {
  width: calc(100% - 4px);
}

/* 現在のステップ */
.step.is-current .step__circle {
  background: rgba(56, 189, 248, 0.16);
  border-color: #38bdf8;
  color: #7dd3fc;
  box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.14);
  transform: scale(1.06);
}

.step.is-current .step__label {
  color: #eef1f6;
  font-weight: 600;
}

/* 完了したステップ */
.step.is-complete .step__circle {
  background: linear-gradient(140deg, #34d399, #22c07a);
  border-color: transparent;
}

.step.is-complete .step__num {
  opacity: 0;
  transform: scale(0.5);
}

.step.is-complete .step__check {
  opacity: 1;
  transform: scale(1);
}

.step.is-complete .step__label {
  color: rgba(238, 241, 246, 0.75);
}

.wizard__actions {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 26px;
}

.btn {
  flex: 1 1 auto;
  padding: 10px 16px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 12px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: transform 0.15s ease, background-color 0.2s ease, opacity 0.2s ease;
}

.btn:active {
  transform: scale(0.97);
}

.btn--ghost {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.14);
  color: rgba(238, 241, 246, 0.75);
}

.btn--ghost:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.btn--primary {
  background: linear-gradient(135deg, #38bdf8, #34d399);
  color: #0b1220;
}

@media (max-width: 380px) {
  .wizard { padding: 24px 16px 18px; }
  .step { width: 52px; }
}

【JavaScript】
// ステップインジケータ: 現在地を色分けし、接続線を充填する。自動進行とNext/Backの手動操作を両立させる
(() => {
  const root = document.getElementById('ssiSteps');
  const backBtn = document.getElementById('ssiBack');
  const nextBtn = document.getElementById('ssiNext');
  if (!root) return; // null安全

  const stepsEls = Array.from(root.querySelectorAll('.step'));
  const connectors = Array.from(root.querySelectorAll('.connector'));
  if (!stepsEls.length) return;

  const TOTAL = stepsEls.length;
  const LABELS = stepsEls.map((el) => el.querySelector('.step__label')?.textContent?.trim() || '');
  const AUTO_MS = 2400;
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let current = 1;
  let autoTimer = null;

  function render() {
    stepsEls.forEach((el, i) => {
      const n = i + 1;
      el.classList.toggle('is-complete', n < current);
      el.classList.toggle('is-current', n === current);
      el.classList.toggle('is-upcoming', n > current);
    });
    connectors.forEach((el, i) => {
      el.classList.toggle('is-filled', i + 1 < current);
    });

    root.setAttribute('aria-valuenow', String(current));
    root.setAttribute('aria-valuetext', `ステップ ${current} / ${TOTAL}: ${LABELS[current - 1]}`);

    if (backBtn) backBtn.disabled = current === 1;
    if (nextBtn) nextBtn.textContent = current === TOTAL ? '完了' : '次へ';
  }

  function goTo(n) {
    // 1〜TOTALの範囲でループさせる(完了の次はアカウントへ)
    current = ((n - 1 + TOTAL) % TOTAL) + 1;
    render();
  }

  function restartAuto() {
    if (reduce) return;
    clearInterval(autoTimer);
    autoTimer = setInterval(() => goTo(current + 1), AUTO_MS);
  }

  nextBtn?.addEventListener('click', () => {
    goTo(current + 1);
    restartAuto();
  });
  backBtn?.addEventListener('click', () => {
    goTo(current - 1);
    restartAuto();
  });

  render();
  restartAuto();
})();

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

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