円形カウントダウン

SVGリングのstroke-dashoffsetを操作し残り秒を可視化する円形タイマー。完了でリングと数字が赤に変化します。リダイレクトや自動遷移の予告に。

#svg#javascript#countdown

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura 新曲MVプレミア公開:円形カウントダウンで公開までの残り秒を可視化 -->
<div class="sk-premiere">
  <header class="sk-premiere__bar">
    <span class="sk-premiere__name">🌸 Sakura</span>
    <span class="sk-premiere__tag">MV PREMIERE</span>
  </header>

  <div class="sk-premiere__mv" style="--img:url('https://picsum.photos/420/260?random=61')">
    <span class="sk-premiere__title">「春風メロディー」</span>
    <span class="sk-premiere__sub">7th Single Music Video</span>

    <!-- 円形カウントダウン(主役) -->
    <div class="sk-ring">
      <svg viewBox="0 0 120 120">
        <circle class="sk-ring__bg" cx="60" cy="60" r="52"/>
        <circle class="sk-ring__fg" id="skRingFg" cx="60" cy="60" r="52"/>
      </svg>
      <div class="sk-ring__center">
        <span class="sk-ring__num" id="skNum">10</span>
        <span class="sk-ring__lbl">公開まで</span>
      </div>
    </div>
  </div>

  <p class="sk-premiere__note" id="skNote">まもなくプレミア公開がはじまります</p>
</div>
CSS
/* Sakura MVプレミア:円形カウントダウンリング */
:root {
  --pink: #ffd1e0;
  --pink2: #ff8fb3;
  --ink: #4a4450;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: linear-gradient(180deg, #fff 0%, var(--pink) 100%);
  color: var(--ink);
  overflow: hidden;
}

.sk-premiere {
  width: 340px;
  background: #fff;
  border-radius: 20px;
  padding: 14px;
  box-shadow: 0 16px 40px rgba(255,143,179,0.3);
}

.sk-premiere__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2px 4px 12px;
}
.sk-premiere__name { font-size: 15px; font-weight: 700; color: #7a3b53; }
.sk-premiere__tag { font-size: 10px; letter-spacing: 0.2em; color: var(--pink2); font-weight: 700; }

/* MVサムネ上にリングを重ねる */
.sk-premiere__mv {
  position: relative;
  height: 230px;
  border-radius: 14px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #fff;
  text-align: center;
  background:
    linear-gradient(180deg, rgba(122,59,83,0.35), rgba(122,59,83,0.6)),
    var(--img) center/cover no-repeat;
}
.sk-premiere__title { font-size: 19px; font-weight: 700; text-shadow: 0 2px 8px rgba(0,0,0,0.4); }
.sk-premiere__sub { font-size: 11px; opacity: 0.9; margin-top: 2px; }

/* リング */
.sk-ring { position: relative; width: 120px; height: 120px; margin-top: 14px; }
.sk-ring svg { width: 120px; height: 120px; transform: rotate(-90deg); }
.sk-ring__bg {
  fill: none;
  stroke: rgba(255,255,255,0.3);
  stroke-width: 7;
}
.sk-ring__fg {
  fill: none;
  stroke: #fff;
  stroke-width: 7;
  stroke-linecap: round;
  /* JSで stroke-dasharray / offset を制御 */
  transition: stroke-dashoffset 1s linear, stroke 0.3s ease;
}
.sk-ring.is-done .sk-ring__fg { stroke: #ff4d6d; }

.sk-ring__center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.sk-ring__num { font-size: 38px; font-weight: 800; line-height: 1; text-shadow: 0 2px 6px rgba(0,0,0,0.3); }
.sk-ring.is-done .sk-ring__num { color: #ff4d6d; }
.sk-ring__lbl { font-size: 10px; opacity: 0.9; margin-top: 4px; letter-spacing: 0.1em; }

.sk-premiere__note { margin: 12px 4px 4px; font-size: 11.5px; color: #9a8f96; text-align: center; }
.sk-premiere__note.is-done { color: var(--pink2); font-weight: 700; }

@media (prefers-reduced-motion: reduce) {
  .sk-ring__fg { transition: stroke 0.3s ease; }
}
JavaScript
// Sakura MVプレミア:SVGリングの stroke-dashoffset で残り秒を可視化(ループ)
const fg = document.getElementById('skRingFg');
const num = document.getElementById('skNum');
const note = document.getElementById('skNote');
const ring = fg ? fg.closest('.sk-ring') : null;

const R = 52;
const CIRC = 2 * Math.PI * R; // 円周
const TOTAL = 10; // 秒
let remain = TOTAL;
let timer = null;

if (fg) fg.style.strokeDasharray = CIRC.toFixed(1);

function render() {
  if (!fg || !num) return;
  // 残り割合に応じてリングを減らす
  const ratio = remain / TOTAL;
  fg.style.strokeDashoffset = (CIRC * (1 - ratio)).toFixed(1);
  num.textContent = remain;
}

function tick() {
  remain -= 1;
  render();
  if (remain <= 0) {
    clearInterval(timer);
    ring?.classList.add('is-done');
    if (num) num.textContent = '0';
    if (note) { note.textContent = 'プレミア公開がはじまりました!🌸'; note.classList.add('is-done'); }
    // しばらくして再カウント(ループ)
    setTimeout(start, 2800);
  }
}

function start() {
  remain = TOTAL;
  clearInterval(timer);
  ring?.classList.remove('is-done');
  if (note) { note.textContent = 'まもなくプレミア公開がはじまります'; note.classList.remove('is-done'); }
  render();
  timer = setInterval(tick, 1000);
}

// 初回起動
start();

コード

HTML
<!-- 円形カウントダウン: SVGリングが減りながら残り秒を表示。完了で色が変化 -->
<div class="cd-stage">
  <div class="cd-wrap">
    <svg class="cd-svg" viewBox="0 0 120 120" aria-hidden="true">
      <defs>
        <linearGradient id="cdGrad" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stop-color="#34d399"/>
          <stop offset="50%" stop-color="#22d3ee"/>
          <stop offset="100%" stop-color="#818cf8"/>
        </linearGradient>
      </defs>
      <!-- 背景トラック -->
      <circle class="cd-track" cx="60" cy="60" r="52"/>
      <!-- 進捗リング(JSで dashoffset を制御) -->
      <circle class="cd-ring" id="cdRing" cx="60" cy="60" r="52"/>
    </svg>
    <div class="cd-center">
      <span class="cd-num" id="cdNum">10</span>
      <span class="cd-unit">SEC</span>
    </div>
  </div>
  <button class="cd-btn" id="cdBtn" type="button">スタート</button>
</div>
CSS
:root {
  --bg: #0a0f1f;
  --track: rgba(255, 255, 255, .08);
  --txt: #eaf0ff;
  --done: #f87171;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(700px 500px at 50% -10%, #16224a 0%, transparent 60%),
    var(--bg);
  color: var(--txt);
}
.cd-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  padding: 24px;
}
.cd-wrap {
  position: relative;
  width: 180px;
  height: 180px;
  display: grid;
  place-items: center;
}
.cd-svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg); /* 12時方向から開始 */
}
.cd-track {
  fill: none;
  stroke: var(--track);
  stroke-width: 9;
}
.cd-ring {
  fill: none;
  stroke: url(#cdGrad);
  stroke-width: 9;
  stroke-linecap: round;
  /* 円周 ≒ 2πr = 326.7。JSで stroke-dashoffset を変えて減らす */
  stroke-dasharray: 326.7;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1s linear, stroke .4s ease;
  filter: drop-shadow(0 0 6px rgba(56, 189, 248, .5));
}
.cd-wrap.is-done .cd-ring { stroke: var(--done); }

.cd-center {
  position: absolute;
  display: grid;
  justify-items: center;
  gap: 2px;
}
.cd-num {
  font-size: 52px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.cd-unit {
  font-size: 11px;
  letter-spacing: .3em;
  color: rgba(234, 240, 255, .5);
  padding-left: .3em;
}
.cd-wrap.is-done .cd-num { color: var(--done); animation: cd-pop .5s ease; }
@keyframes cd-pop { 0% { transform: scale(.6); } 60% { transform: scale(1.15); } 100% { transform: scale(1); } }

.cd-btn {
  border: 1px solid rgba(255, 255, 255, .16);
  background: rgba(255, 255, 255, .04);
  color: var(--txt);
  padding: 10px 26px;
  border-radius: 999px;
  font-size: 13px;
  letter-spacing: .04em;
  cursor: pointer;
  transition: background .2s, transform .1s;
}
.cd-btn:hover { background: rgba(255, 255, 255, .12); }
.cd-btn:active { transform: scale(.95); }

@media (prefers-reduced-motion: reduce) {
  .cd-ring { transition: stroke-dashoffset .3s linear, stroke .3s; }
  .cd-wrap.is-done .cd-num { animation: none; }
}
JavaScript
// SVGリングで表す円形カウントダウン
const ring = document.getElementById('cdRing');
const num  = document.getElementById('cdNum');
const btn  = document.getElementById('cdBtn');
const wrap = document.querySelector('.cd-wrap');

const TOTAL = 10;                 // 秒数
const CIRC  = 2 * Math.PI * 52;   // 円周 ≒ 326.7
let remain = TOTAL;
let timer  = null;

// リングと数字を現在の残り秒に合わせて更新
function render() {
  const ratio = remain / TOTAL;
  if (ring) ring.style.strokeDashoffset = String(CIRC * (1 - ratio));
  if (num)  num.textContent = String(remain);
}

function step() {
  remain -= 1;
  render();
  if (remain <= 0) {
    clearInterval(timer);
    timer = null;
    wrap?.classList.add('is-done');
    if (num) num.textContent = '0';
    if (btn) btn.textContent = 'リセット';
  }
}

function start() {
  clearInterval(timer);
  wrap?.classList.remove('is-done');
  remain = TOTAL;
  render();
  if (btn) btn.textContent = '実行中…';
  timer = setInterval(step, 1000);
}

btn?.addEventListener('click', start);

// 初期描画(満タン表示)
render();

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

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

# 追加してほしい効果
円形カウントダウン(ローダー & スケルトン)
SVGリングのstroke-dashoffsetを操作し残り秒を可視化する円形タイマー。完了でリングと数字が赤に変化します。リダイレクトや自動遷移の予告に。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 円形カウントダウン: SVGリングが減りながら残り秒を表示。完了で色が変化 -->
<div class="cd-stage">
  <div class="cd-wrap">
    <svg class="cd-svg" viewBox="0 0 120 120" aria-hidden="true">
      <defs>
        <linearGradient id="cdGrad" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stop-color="#34d399"/>
          <stop offset="50%" stop-color="#22d3ee"/>
          <stop offset="100%" stop-color="#818cf8"/>
        </linearGradient>
      </defs>
      <!-- 背景トラック -->
      <circle class="cd-track" cx="60" cy="60" r="52"/>
      <!-- 進捗リング(JSで dashoffset を制御) -->
      <circle class="cd-ring" id="cdRing" cx="60" cy="60" r="52"/>
    </svg>
    <div class="cd-center">
      <span class="cd-num" id="cdNum">10</span>
      <span class="cd-unit">SEC</span>
    </div>
  </div>
  <button class="cd-btn" id="cdBtn" type="button">スタート</button>
</div>

【CSS】
:root {
  --bg: #0a0f1f;
  --track: rgba(255, 255, 255, .08);
  --txt: #eaf0ff;
  --done: #f87171;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(700px 500px at 50% -10%, #16224a 0%, transparent 60%),
    var(--bg);
  color: var(--txt);
}
.cd-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  padding: 24px;
}
.cd-wrap {
  position: relative;
  width: 180px;
  height: 180px;
  display: grid;
  place-items: center;
}
.cd-svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg); /* 12時方向から開始 */
}
.cd-track {
  fill: none;
  stroke: var(--track);
  stroke-width: 9;
}
.cd-ring {
  fill: none;
  stroke: url(#cdGrad);
  stroke-width: 9;
  stroke-linecap: round;
  /* 円周 ≒ 2πr = 326.7。JSで stroke-dashoffset を変えて減らす */
  stroke-dasharray: 326.7;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1s linear, stroke .4s ease;
  filter: drop-shadow(0 0 6px rgba(56, 189, 248, .5));
}
.cd-wrap.is-done .cd-ring { stroke: var(--done); }

.cd-center {
  position: absolute;
  display: grid;
  justify-items: center;
  gap: 2px;
}
.cd-num {
  font-size: 52px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.cd-unit {
  font-size: 11px;
  letter-spacing: .3em;
  color: rgba(234, 240, 255, .5);
  padding-left: .3em;
}
.cd-wrap.is-done .cd-num { color: var(--done); animation: cd-pop .5s ease; }
@keyframes cd-pop { 0% { transform: scale(.6); } 60% { transform: scale(1.15); } 100% { transform: scale(1); } }

.cd-btn {
  border: 1px solid rgba(255, 255, 255, .16);
  background: rgba(255, 255, 255, .04);
  color: var(--txt);
  padding: 10px 26px;
  border-radius: 999px;
  font-size: 13px;
  letter-spacing: .04em;
  cursor: pointer;
  transition: background .2s, transform .1s;
}
.cd-btn:hover { background: rgba(255, 255, 255, .12); }
.cd-btn:active { transform: scale(.95); }

@media (prefers-reduced-motion: reduce) {
  .cd-ring { transition: stroke-dashoffset .3s linear, stroke .3s; }
  .cd-wrap.is-done .cd-num { animation: none; }
}

【JavaScript】
// SVGリングで表す円形カウントダウン
const ring = document.getElementById('cdRing');
const num  = document.getElementById('cdNum');
const btn  = document.getElementById('cdBtn');
const wrap = document.querySelector('.cd-wrap');

const TOTAL = 10;                 // 秒数
const CIRC  = 2 * Math.PI * 52;   // 円周 ≒ 326.7
let remain = TOTAL;
let timer  = null;

// リングと数字を現在の残り秒に合わせて更新
function render() {
  const ratio = remain / TOTAL;
  if (ring) ring.style.strokeDashoffset = String(CIRC * (1 - ratio));
  if (num)  num.textContent = String(remain);
}

function step() {
  remain -= 1;
  render();
  if (remain <= 0) {
    clearInterval(timer);
    timer = null;
    wrap?.classList.add('is-done');
    if (num) num.textContent = '0';
    if (btn) btn.textContent = 'リセット';
  }
}

function start() {
  clearInterval(timer);
  wrap?.classList.remove('is-done');
  remain = TOTAL;
  render();
  if (btn) btn.textContent = '実行中…';
  timer = setInterval(step, 1000);
}

btn?.addEventListener('click', start);

// 初期描画(満タン表示)
render();

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

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