進捗ステップ
受付から配達完了までを追う注文追跡ステップ。時間の経過とともに現在地が自動で進み、完了した区間の連結線が満ちていきます。
ライブデモ
コード
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">✓</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">✓</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">✓</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">✓</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">✓</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">✓</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">✓</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">✓</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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。