進捗ステップ

受付から配達完了までを追う注文追跡ステップ。時間の経過とともに現在地が自動で進み、完了した区間の連結線が満ちていきます。

#css#js#timeline

ライブデモ

コード

HTML
<!-- 進捗ステップ: 注文追跡風の水平ステップ。完了区間の連結線が満ちていく -->
<section class="tsp">
  <div class="tsp__card">
    <div class="tsp__head">
      <p class="tsp__order">注文番号 #A20260704-013</p>
      <h2 class="tsp__title">ご注文の配送状況</h2>
    </div>

    <div class="tsp__track">
      <div class="tsp__line" aria-hidden="true">
        <span class="tsp__line-fill"></span>
      </div>
      <ol class="tsp__steps">
        <li class="tsp__step" data-idx="0">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">受付</span>
        </li>
        <li class="tsp__step" data-idx="1">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">発送</span>
        </li>
        <li class="tsp__step" data-idx="2">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">配達中</span>
        </li>
        <li class="tsp__step" data-idx="3">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">完了</span>
        </li>
      </ol>
    </div>

    <p class="tsp__status" role="status">現在のステータス: <b class="tsp__status-word">受付</b></p>
    <p class="tsp__eta">お届け予定日: 7月6日(月)</p>
  </div>
</section>
CSS
/* 進捗ステップ: 注文追跡風の水平ステップ。完了区間の連結線がJSで満ちていく */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 28px 16px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", "Yu Gothic UI", system-ui, sans-serif;
  background: linear-gradient(160deg, #f6f8fb 0%, #e9edf7 100%);
  color: #1e2233;
}

/* ルート: 全幅 */
.tsp { width: 100%; display: grid; place-items: center; }

.tsp__card {
  width: min(520px, 100%);
  background: #fff;
  border-radius: 20px;
  padding: 26px 26px 22px;
  box-shadow: 0 26px 56px -28px rgba(24, 28, 60, .28), 0 2px 8px rgba(24, 28, 60, .05);
  text-align: center;
  transition: opacity .3s ease;
}
.tsp__card.is-resetting { opacity: .35; }

.tsp__head { margin-bottom: 26px; }
.tsp__order {
  margin: 0 0 6px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .05em;
  color: #8b90ab;
}
.tsp__title { margin: 0; font-size: 18px; font-weight: 800; }

.tsp__track { position: relative; padding: 0 6px; margin-bottom: 22px; }
.tsp__line {
  position: absolute;
  left: 34px; right: 34px; top: 18px;
  height: 4px;
  border-radius: 999px;
  background: #e7eaf4;
  overflow: hidden;
}
.tsp__line-fill {
  display: block;
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, #2fbf7a, #22d3ee);
  transition: width .7s cubic-bezier(.22, 1, .36, 1);
}

.tsp__steps {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: space-between;
}

.tsp__step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 25%;
  min-width: 0;
}

.tsp__circle {
  position: relative;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #e2e6f2;
  display: grid;
  place-items: center;
  transition: border-color .35s ease, background .35s ease, box-shadow .35s ease;
}
.tsp__check {
  font-size: 15px;
  font-weight: 800;
  color: #fff;
  opacity: 0;
  transform: scale(.4);
  transition: opacity .25s ease, transform .25s cubic-bezier(.22, 1, .36, 1);
}

.tsp__label {
  font-size: 12.5px;
  font-weight: 700;
  color: #9297b3;
  white-space: nowrap;
  transition: color .3s ease;
}

/* 完了ステップ */
.tsp__step.is-done .tsp__circle {
  background: linear-gradient(135deg, #2fbf7a, #22d3ee);
  border-color: transparent;
}
.tsp__step.is-done .tsp__check { opacity: 1; transform: scale(1); }
.tsp__step.is-done .tsp__label { color: #23273a; }

/* 現在地ステップ: パルスするリング */
.tsp__step.is-current .tsp__circle {
  border-color: #22d3ee;
  box-shadow: 0 0 0 5px rgba(34, 211, 238, .18);
  animation: tspPulse 1.6s ease-in-out infinite;
}
.tsp__step.is-current .tsp__circle::after {
  content: "";
  width: 10px; height: 10px;
  border-radius: 50%;
  background: #22d3ee;
}
.tsp__step.is-current .tsp__label { color: #1494a8; }

.tsp__status { margin: 0 0 4px; font-size: 13.5px; color: #4a4f68; }
.tsp__status b { color: #16a190; }
.tsp__eta { margin: 0; font-size: 12px; color: #9297b3; }

@keyframes tspPulse {
  0%, 100% { box-shadow: 0 0 0 5px rgba(34, 211, 238, .18); }
  50% { box-shadow: 0 0 0 9px rgba(34, 211, 238, 0); }
}

@media (max-width: 420px) {
  .tsp__card { padding: 20px 16px 18px; border-radius: 16px; }
  .tsp__line { left: 22px; right: 22px; }
  .tsp__circle { width: 30px; height: 30px; }
  .tsp__label { font-size: 11px; }
}

@media (prefers-reduced-motion: reduce) {
  .tsp__step.is-current .tsp__circle { animation: none; }
  .tsp__line-fill, .tsp__circle, .tsp__check, .tsp__label, .tsp__card { transition: none; }
}
JavaScript
// 進捗ステップ: 受付→発送→配達中→完了を自動で進め、連結線の充填とラベルを同期させる
(() => {
  const card = document.querySelector('.tsp__card');
  const steps = Array.from(document.querySelectorAll('.tsp__step'));
  const fill = document.querySelector('.tsp__line-fill');
  const statusWord = document.querySelector('.tsp__status-word');
  const eta = document.querySelector('.tsp__eta');
  if (!card || !steps.length || !fill || !statusWord) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const LABELS = ['受付', '発送', '配達中', '完了'];
  const ETA_DEFAULT = 'お届け予定日: 7月6日(月)';
  const ETA_DONE = '本日、配達を完了しました';
  const STEP_MS = 1800;  // 次のステップへ進むまでの間隔
  const HOLD_MS = 2600;  // 完了状態で静止する時間

  let current = 0;

  const render = () => {
    steps.forEach((el, i) => {
      el.classList.toggle('is-done', i < current);
      el.classList.toggle('is-current', i === current);
    });
    fill.style.width = (current / (steps.length - 1)) * 100 + '%';
    statusWord.textContent = LABELS[current] || '';
    if (eta) eta.textContent = current === steps.length - 1 ? ETA_DONE : ETA_DEFAULT;
  };

  render();
  if (reduce) return; // 動きを抑える設定では初期状態のまま止める

  let timer = null;
  const tick = () => {
    if (current < steps.length - 1) {
      current++;
      render();
      timer = setTimeout(tick, STEP_MS);
    } else {
      // 完了状態でしばらく静止したあと、軽くフェードしてリセットしループする
      timer = setTimeout(() => {
        card.classList.add('is-resetting');
        timer = setTimeout(() => {
          current = 0;
          render();
          card.classList.remove('is-resetting');
          timer = setTimeout(tick, STEP_MS);
        }, 320);
      }, HOLD_MS);
    }
  };
  timer = setTimeout(tick, STEP_MS);

  window.addEventListener('pagehide', () => clearTimeout(timer));
})();

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

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

# 追加してほしい効果
進捗ステップ(タイムライン & 404)
受付から配達完了までを追う注文追跡ステップ。時間の経過とともに現在地が自動で進み、完了した区間の連結線が満ちていきます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 進捗ステップ: 注文追跡風の水平ステップ。完了区間の連結線が満ちていく -->
<section class="tsp">
  <div class="tsp__card">
    <div class="tsp__head">
      <p class="tsp__order">注文番号 #A20260704-013</p>
      <h2 class="tsp__title">ご注文の配送状況</h2>
    </div>

    <div class="tsp__track">
      <div class="tsp__line" aria-hidden="true">
        <span class="tsp__line-fill"></span>
      </div>
      <ol class="tsp__steps">
        <li class="tsp__step" data-idx="0">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">受付</span>
        </li>
        <li class="tsp__step" data-idx="1">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">発送</span>
        </li>
        <li class="tsp__step" data-idx="2">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">配達中</span>
        </li>
        <li class="tsp__step" data-idx="3">
          <span class="tsp__circle"><span class="tsp__check" aria-hidden="true">&#10003;</span></span>
          <span class="tsp__label">完了</span>
        </li>
      </ol>
    </div>

    <p class="tsp__status" role="status">現在のステータス: <b class="tsp__status-word">受付</b></p>
    <p class="tsp__eta">お届け予定日: 7月6日(月)</p>
  </div>
</section>

【CSS】
/* 進捗ステップ: 注文追跡風の水平ステップ。完了区間の連結線がJSで満ちていく */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 28px 16px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", "Yu Gothic UI", system-ui, sans-serif;
  background: linear-gradient(160deg, #f6f8fb 0%, #e9edf7 100%);
  color: #1e2233;
}

/* ルート: 全幅 */
.tsp { width: 100%; display: grid; place-items: center; }

.tsp__card {
  width: min(520px, 100%);
  background: #fff;
  border-radius: 20px;
  padding: 26px 26px 22px;
  box-shadow: 0 26px 56px -28px rgba(24, 28, 60, .28), 0 2px 8px rgba(24, 28, 60, .05);
  text-align: center;
  transition: opacity .3s ease;
}
.tsp__card.is-resetting { opacity: .35; }

.tsp__head { margin-bottom: 26px; }
.tsp__order {
  margin: 0 0 6px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .05em;
  color: #8b90ab;
}
.tsp__title { margin: 0; font-size: 18px; font-weight: 800; }

.tsp__track { position: relative; padding: 0 6px; margin-bottom: 22px; }
.tsp__line {
  position: absolute;
  left: 34px; right: 34px; top: 18px;
  height: 4px;
  border-radius: 999px;
  background: #e7eaf4;
  overflow: hidden;
}
.tsp__line-fill {
  display: block;
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, #2fbf7a, #22d3ee);
  transition: width .7s cubic-bezier(.22, 1, .36, 1);
}

.tsp__steps {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: space-between;
}

.tsp__step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 25%;
  min-width: 0;
}

.tsp__circle {
  position: relative;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #e2e6f2;
  display: grid;
  place-items: center;
  transition: border-color .35s ease, background .35s ease, box-shadow .35s ease;
}
.tsp__check {
  font-size: 15px;
  font-weight: 800;
  color: #fff;
  opacity: 0;
  transform: scale(.4);
  transition: opacity .25s ease, transform .25s cubic-bezier(.22, 1, .36, 1);
}

.tsp__label {
  font-size: 12.5px;
  font-weight: 700;
  color: #9297b3;
  white-space: nowrap;
  transition: color .3s ease;
}

/* 完了ステップ */
.tsp__step.is-done .tsp__circle {
  background: linear-gradient(135deg, #2fbf7a, #22d3ee);
  border-color: transparent;
}
.tsp__step.is-done .tsp__check { opacity: 1; transform: scale(1); }
.tsp__step.is-done .tsp__label { color: #23273a; }

/* 現在地ステップ: パルスするリング */
.tsp__step.is-current .tsp__circle {
  border-color: #22d3ee;
  box-shadow: 0 0 0 5px rgba(34, 211, 238, .18);
  animation: tspPulse 1.6s ease-in-out infinite;
}
.tsp__step.is-current .tsp__circle::after {
  content: "";
  width: 10px; height: 10px;
  border-radius: 50%;
  background: #22d3ee;
}
.tsp__step.is-current .tsp__label { color: #1494a8; }

.tsp__status { margin: 0 0 4px; font-size: 13.5px; color: #4a4f68; }
.tsp__status b { color: #16a190; }
.tsp__eta { margin: 0; font-size: 12px; color: #9297b3; }

@keyframes tspPulse {
  0%, 100% { box-shadow: 0 0 0 5px rgba(34, 211, 238, .18); }
  50% { box-shadow: 0 0 0 9px rgba(34, 211, 238, 0); }
}

@media (max-width: 420px) {
  .tsp__card { padding: 20px 16px 18px; border-radius: 16px; }
  .tsp__line { left: 22px; right: 22px; }
  .tsp__circle { width: 30px; height: 30px; }
  .tsp__label { font-size: 11px; }
}

@media (prefers-reduced-motion: reduce) {
  .tsp__step.is-current .tsp__circle { animation: none; }
  .tsp__line-fill, .tsp__circle, .tsp__check, .tsp__label, .tsp__card { transition: none; }
}

【JavaScript】
// 進捗ステップ: 受付→発送→配達中→完了を自動で進め、連結線の充填とラベルを同期させる
(() => {
  const card = document.querySelector('.tsp__card');
  const steps = Array.from(document.querySelectorAll('.tsp__step'));
  const fill = document.querySelector('.tsp__line-fill');
  const statusWord = document.querySelector('.tsp__status-word');
  const eta = document.querySelector('.tsp__eta');
  if (!card || !steps.length || !fill || !statusWord) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const LABELS = ['受付', '発送', '配達中', '完了'];
  const ETA_DEFAULT = 'お届け予定日: 7月6日(月)';
  const ETA_DONE = '本日、配達を完了しました';
  const STEP_MS = 1800;  // 次のステップへ進むまでの間隔
  const HOLD_MS = 2600;  // 完了状態で静止する時間

  let current = 0;

  const render = () => {
    steps.forEach((el, i) => {
      el.classList.toggle('is-done', i < current);
      el.classList.toggle('is-current', i === current);
    });
    fill.style.width = (current / (steps.length - 1)) * 100 + '%';
    statusWord.textContent = LABELS[current] || '';
    if (eta) eta.textContent = current === steps.length - 1 ? ETA_DONE : ETA_DEFAULT;
  };

  render();
  if (reduce) return; // 動きを抑える設定では初期状態のまま止める

  let timer = null;
  const tick = () => {
    if (current < steps.length - 1) {
      current++;
      render();
      timer = setTimeout(tick, STEP_MS);
    } else {
      // 完了状態でしばらく静止したあと、軽くフェードしてリセットしループする
      timer = setTimeout(() => {
        card.classList.add('is-resetting');
        timer = setTimeout(() => {
          current = 0;
          render();
          card.classList.remove('is-resetting');
          timer = setTimeout(tick, STEP_MS);
        }, 320);
      }, HOLD_MS);
    }
  };
  timer = setTimeout(tick, STEP_MS);

  window.addEventListener('pagehide', () => clearTimeout(timer));
})();

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

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