セグメント進捗

インスタのストーリーズのように分割バーが1本ずつ自動で満ちて次のシーンへ進み、最後まで来ると最初からループする進捗表示です。

#css#js#progress

ライブデモ

コード

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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。