セグメント進捗
インスタのストーリーズのように分割バーが1本ずつ自動で満ちて次のシーンへ進み、最後まで来ると最初からループする進捗表示です。
ライブデモ
コード
HTML
<!-- セグメント進捗: ストーリーズ風の分割バーが1本ずつ自動で満ちて次のシーンへ進み、ループする -->
<div class="stage">
<div class="story">
<div class="story__scene" id="segScene"></div>
<div class="story__overlay">
<div class="story__bars" id="segBars" role="progressbar" aria-valuenow="1" aria-valuemin="1" aria-valuemax="5" aria-label="ストーリーの再生位置">
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
</div>
<div class="story__header">
<span class="story__avatar" aria-hidden="true"></span>
<span class="story__user">wedelab</span>
<span class="story__index" id="segIndex">1 / 5</span>
</div>
</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 20% 15%, #201830 0%, transparent 50%),
radial-gradient(circle at 85% 85%, #101c2a 0%, transparent 52%),
#08090f;
color: #f5f6fb;
}
.stage {
width: 100%;
display: grid;
place-items: center;
}
.story {
position: relative;
width: min(220px, 78vw);
aspect-ratio: 9 / 16;
border-radius: 22px;
overflow: hidden;
box-shadow: 0 26px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.story__scene {
position: absolute;
inset: -6%;
background: linear-gradient(150deg, #3a3f8c, #7c3aed 55%, #db2777);
transition: background 0.8s ease;
animation: seg-breathe 7s ease-in-out infinite;
}
@keyframes seg-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
.story__scene.t0 { background: linear-gradient(150deg, #3a3f8c, #7c3aed 55%, #db2777); }
.story__scene.t1 { background: linear-gradient(150deg, #0ea5a5, #2563eb 55%, #4338ca); }
.story__scene.t2 { background: linear-gradient(150deg, #ea580c, #db2777 55%, #7c3aed); }
.story__scene.t3 { background: linear-gradient(150deg, #16a34a, #0ea5a5 55%, #2563eb); }
.story__scene.t4 { background: linear-gradient(150deg, #db2777, #ea580c 55%, #ca8a04); }
.story__overlay {
position: absolute;
inset: 0;
padding: 12px 12px 0;
display: flex;
flex-direction: column;
gap: 10px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.32) 0%, transparent 32%);
}
.story__bars {
display: flex;
gap: 5px;
}
.story__bar {
position: relative;
flex: 1 1 0;
height: 3px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.28);
overflow: hidden;
}
.story__bar-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
background: #ffffff;
}
.story__header {
display: flex;
align-items: center;
gap: 8px;
}
.story__avatar {
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, #fde68a, #fb7185);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55);
flex: 0 0 auto;
}
.story__user {
font-size: 12px;
font-weight: 700;
color: #fff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.story__index {
margin-left: auto;
font-size: 10.5px;
font-variant-numeric: tabular-nums;
color: rgba(255, 255, 255, 0.75);
}
@media (prefers-reduced-motion: reduce) {
.story__scene { animation: none; }
}
JavaScript
// セグメント進捗: ストーリーズ風に1本ずつバーを満たし、シーン(背景)を切り替えながらループする
(() => {
const root = document.getElementById('segBars');
const scene = document.getElementById('segScene');
const indexEl = document.getElementById('segIndex');
if (!root) return; // null安全
const bars = Array.from(root.querySelectorAll('.story__bar-fill'));
if (!bars.length) return;
const TOTAL = bars.length;
const DURATION = 2800; // ms、1セグメントが満ちるまでの時間
const THEMES = ['t0', 't1', 't2', 't3', 't4'];
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let idx = 0;
let timer = null;
function paint() {
// 完了済みは満タン、これから来る分は空にリセット(トランジションなし)
bars.forEach((b, i) => {
b.style.transition = 'none';
b.style.width = i < idx ? '100%' : '0%';
});
// 強制リフローしてから、現在のセグメントだけにtransitionを設定して満たす
void root.offsetWidth;
bars.forEach((b, i) => {
b.style.transition = i === idx ? `width ${DURATION}ms linear` : 'none';
});
requestAnimationFrame(() => {
if (bars[idx]) bars[idx].style.width = '100%';
});
if (scene) scene.className = 'story__scene ' + THEMES[idx % THEMES.length];
if (indexEl) indexEl.textContent = `${idx + 1} / ${TOTAL}`;
root.setAttribute('aria-valuenow', String(idx + 1));
}
function advance() {
idx = (idx + 1) % TOTAL;
paint();
schedule();
}
function schedule() {
clearTimeout(timer);
timer = setTimeout(advance, DURATION);
}
if (reduce) {
// モーション抑制時は現在地のみ静的に表示し、数秒おきに切り替える(点滅させない)
idx = 0;
bars.forEach((b, i) => { b.style.width = i === 0 ? '100%' : '0%'; });
if (scene) scene.className = 'story__scene ' + THEMES[0];
if (indexEl) indexEl.textContent = `1 / ${TOTAL}`;
root.setAttribute('aria-valuenow', '1');
setInterval(() => {
idx = (idx + 1) % TOTAL;
bars.forEach((b, i) => { b.style.width = i === idx ? '100%' : '0%'; });
if (scene) scene.className = 'story__scene ' + THEMES[idx % THEMES.length];
if (indexEl) indexEl.textContent = `${idx + 1} / ${TOTAL}`;
root.setAttribute('aria-valuenow', String(idx + 1));
}, DURATION);
return;
}
paint();
schedule();
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「セグメント進捗」の効果を追加してください。
# 追加してほしい効果
セグメント進捗(カレンダー & ステップ)
インスタのストーリーズのように分割バーが1本ずつ自動で満ちて次のシーンへ進み、最後まで来ると最初からループする進捗表示です。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- セグメント進捗: ストーリーズ風の分割バーが1本ずつ自動で満ちて次のシーンへ進み、ループする -->
<div class="stage">
<div class="story">
<div class="story__scene" id="segScene"></div>
<div class="story__overlay">
<div class="story__bars" id="segBars" role="progressbar" aria-valuenow="1" aria-valuemin="1" aria-valuemax="5" aria-label="ストーリーの再生位置">
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
<span class="story__bar"><span class="story__bar-fill"></span></span>
</div>
<div class="story__header">
<span class="story__avatar" aria-hidden="true"></span>
<span class="story__user">wedelab</span>
<span class="story__index" id="segIndex">1 / 5</span>
</div>
</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 20% 15%, #201830 0%, transparent 50%),
radial-gradient(circle at 85% 85%, #101c2a 0%, transparent 52%),
#08090f;
color: #f5f6fb;
}
.stage {
width: 100%;
display: grid;
place-items: center;
}
.story {
position: relative;
width: min(220px, 78vw);
aspect-ratio: 9 / 16;
border-radius: 22px;
overflow: hidden;
box-shadow: 0 26px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.story__scene {
position: absolute;
inset: -6%;
background: linear-gradient(150deg, #3a3f8c, #7c3aed 55%, #db2777);
transition: background 0.8s ease;
animation: seg-breathe 7s ease-in-out infinite;
}
@keyframes seg-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
.story__scene.t0 { background: linear-gradient(150deg, #3a3f8c, #7c3aed 55%, #db2777); }
.story__scene.t1 { background: linear-gradient(150deg, #0ea5a5, #2563eb 55%, #4338ca); }
.story__scene.t2 { background: linear-gradient(150deg, #ea580c, #db2777 55%, #7c3aed); }
.story__scene.t3 { background: linear-gradient(150deg, #16a34a, #0ea5a5 55%, #2563eb); }
.story__scene.t4 { background: linear-gradient(150deg, #db2777, #ea580c 55%, #ca8a04); }
.story__overlay {
position: absolute;
inset: 0;
padding: 12px 12px 0;
display: flex;
flex-direction: column;
gap: 10px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.32) 0%, transparent 32%);
}
.story__bars {
display: flex;
gap: 5px;
}
.story__bar {
position: relative;
flex: 1 1 0;
height: 3px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.28);
overflow: hidden;
}
.story__bar-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
background: #ffffff;
}
.story__header {
display: flex;
align-items: center;
gap: 8px;
}
.story__avatar {
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, #fde68a, #fb7185);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55);
flex: 0 0 auto;
}
.story__user {
font-size: 12px;
font-weight: 700;
color: #fff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.story__index {
margin-left: auto;
font-size: 10.5px;
font-variant-numeric: tabular-nums;
color: rgba(255, 255, 255, 0.75);
}
@media (prefers-reduced-motion: reduce) {
.story__scene { animation: none; }
}
【JavaScript】
// セグメント進捗: ストーリーズ風に1本ずつバーを満たし、シーン(背景)を切り替えながらループする
(() => {
const root = document.getElementById('segBars');
const scene = document.getElementById('segScene');
const indexEl = document.getElementById('segIndex');
if (!root) return; // null安全
const bars = Array.from(root.querySelectorAll('.story__bar-fill'));
if (!bars.length) return;
const TOTAL = bars.length;
const DURATION = 2800; // ms、1セグメントが満ちるまでの時間
const THEMES = ['t0', 't1', 't2', 't3', 't4'];
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let idx = 0;
let timer = null;
function paint() {
// 完了済みは満タン、これから来る分は空にリセット(トランジションなし)
bars.forEach((b, i) => {
b.style.transition = 'none';
b.style.width = i < idx ? '100%' : '0%';
});
// 強制リフローしてから、現在のセグメントだけにtransitionを設定して満たす
void root.offsetWidth;
bars.forEach((b, i) => {
b.style.transition = i === idx ? `width ${DURATION}ms linear` : 'none';
});
requestAnimationFrame(() => {
if (bars[idx]) bars[idx].style.width = '100%';
});
if (scene) scene.className = 'story__scene ' + THEMES[idx % THEMES.length];
if (indexEl) indexEl.textContent = `${idx + 1} / ${TOTAL}`;
root.setAttribute('aria-valuenow', String(idx + 1));
}
function advance() {
idx = (idx + 1) % TOTAL;
paint();
schedule();
}
function schedule() {
clearTimeout(timer);
timer = setTimeout(advance, DURATION);
}
if (reduce) {
// モーション抑制時は現在地のみ静的に表示し、数秒おきに切り替える(点滅させない)
idx = 0;
bars.forEach((b, i) => { b.style.width = i === 0 ? '100%' : '0%'; });
if (scene) scene.className = 'story__scene ' + THEMES[0];
if (indexEl) indexEl.textContent = `1 / ${TOTAL}`;
root.setAttribute('aria-valuenow', '1');
setInterval(() => {
idx = (idx + 1) % TOTAL;
bars.forEach((b, i) => { b.style.width = i === idx ? '100%' : '0%'; });
if (scene) scene.className = 'story__scene ' + THEMES[idx % THEMES.length];
if (indexEl) indexEl.textContent = `${idx + 1} / ${TOTAL}`;
root.setAttribute('aria-valuenow', String(idx + 1));
}, DURATION);
return;
}
paint();
schedule();
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。