conic円グラフ

CSS conic-gradientだけで作るドーナツ円グラフ。回転マスクで出現させ、割合の内訳表示に最適です。

#css#conic-gradient#chart#animation

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW:在庫管理画面。生豆の構成比をconic円グラフで表示 -->
<section class="mb-stock">
  <header class="mb-bar">
    <div class="mb-brand"><span class="mb-cup">☕</span> MOON BREW</div>
    <span class="mb-tag">在庫レポート</span>
  </header>

  <div class="mb-body">
    <div class="mb-chart">
      <!-- ドーナツ円グラフ(CSS conic-gradient/回転マスクで出現) -->
      <div id="mbPie" class="mb-pie">
        <div class="mb-pie__hole">
          <span class="mb-pie__total" id="mbPieTotal">0%</span>
          <span class="mb-pie__cap">生豆在庫</span>
        </div>
      </div>
    </div>

    <div class="mb-side">
      <h2 class="mb-side__title">産地別 構成比</h2>
      <p class="mb-side__sub">本日入荷ぶんを含む</p>
      <ul id="mbLegend" class="mb-legend"></ul>
    </div>
  </div>
</section>
CSS
/* MOON BREW:在庫構成比(conicドーナツ円グラフが主役) */
:root {
  --cream: #f5ede1;
  --brown: #2b1d12;
  --amber: #c98a3b;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Kaku Gothic ProN", "Yu Gothic", system-ui, sans-serif;
  background: var(--cream);
  color: var(--brown);
}

.mb-stock { height: 400px; padding: 18px 22px; display: flex; flex-direction: column; }

.mb-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.mb-brand {
  font-family: "Hiragino Mincho ProN", serif;
  font-weight: 700; font-size: 16px; letter-spacing: 0.1em;
  display: flex; align-items: center; gap: 7px;
}
.mb-cup { font-size: 17px; }
.mb-tag {
  font-size: 11px; color: var(--amber); letter-spacing: 0.12em; font-weight: 700;
  border: 1px solid rgba(201,138,59,0.4); padding: 4px 11px; border-radius: 14px;
}

.mb-body { flex: 1; display: flex; align-items: center; gap: 26px; }

/* 円グラフ */
.mb-chart { flex: 0 0 auto; display: grid; place-items: center; }
.mb-pie {
  --pie-angle: 0turn;
  --pie-gradient: conic-gradient(#ccc 0turn 1turn);
  width: 168px; height: 168px; border-radius: 50%;
  /* クリーム色の扇でデータを覆い、角度を広げて時計回りに出現 */
  background:
    conic-gradient(from -0.25turn, transparent var(--pie-angle), var(--cream) 0),
    var(--pie-gradient);
  display: grid; place-items: center;
  position: relative;
  filter: drop-shadow(0 10px 24px rgba(43,29,18,0.14));
}
/* 中央の穴(ドーナツ化) */
.mb-pie__hole {
  width: 104px; height: 104px; border-radius: 50%;
  background: var(--cream);
  display: grid; place-items: center; text-align: center;
  box-shadow: inset 0 0 0 1px #e7dccb;
}
.mb-pie__total { font-size: 26px; font-weight: 800; color: var(--brown); line-height: 1; }
.mb-pie__cap { font-size: 10px; color: #8a755e; margin-top: 4px; letter-spacing: 0.08em; }

/* 凡例 */
.mb-side { flex: 1; }
.mb-side__title { margin: 0; font-size: 15px; font-weight: 700; }
.mb-side__sub { margin: 2px 0 12px; font-size: 11px; color: #a08a6f; }
.mb-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.mb-legend li { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.mb-legend .swatch { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
.mb-legend .val { margin-left: auto; font-weight: 700; color: var(--brown); }
JavaScript
// MOON BREW:生豆の産地別構成比をconicドーナツ円グラフで描画
(() => {
  const pie = document.getElementById('mbPie');
  const legend = document.getElementById('mbLegend');
  const totalEl = document.getElementById('mbPieTotal');
  if (!pie || !legend) return; // null安全

  // 産地と在庫量(kg)。値は自動で割合に正規化する
  const data = [
    { label: 'ブラジル', value: 124, color: '#a86f2c' },
    { label: 'エチオピア', value: 88, color: '#c98a3b' },
    { label: 'グアテマラ', value: 64, color: '#7a4f24' },
    { label: 'コロンビア', value: 46, color: '#e0b277' },
  ];
  const total = data.reduce((s, d) => s + d.value, 0);

  // conic-gradientのストップを累積角度で構築
  let acc = 0;
  const stops = data.map((d) => {
    const start = acc / total;
    acc += d.value;
    const end = acc / total;
    return `${d.color} ${start}turn ${end}turn`;
  });
  pie.style.setProperty('--pie-gradient', `conic-gradient(from -0.25turn, ${stops.join(', ')})`);

  // 凡例を生成
  const frag = document.createDocumentFragment();
  data.forEach((d) => {
    const li = document.createElement('li');
    const sw = document.createElement('span');
    sw.className = 'swatch';
    sw.style.background = d.color;
    const name = document.createElement('span');
    name.textContent = d.label;
    const val = document.createElement('span');
    val.className = 'val';
    val.textContent = `${Math.round((d.value / total) * 100)}%`;
    li.append(sw, name, val);
    frag.appendChild(li);
  });
  legend.appendChild(frag);

  if (totalEl) totalEl.textContent = `${total}kg`;

  // 回転マスクで時計回りに出現(reduced-motion配慮)
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduceMotion) {
    pie.style.setProperty('--pie-angle', '1turn');
    return;
  }

  const start = performance.now();
  const duration = 1100;
  function tick(now) {
    const t = Math.min(1, (now - start) / duration);
    const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
    pie.style.setProperty('--pie-angle', `${eased}turn`);
    if (t < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
})();

コード

HTML
<div class="dv-wrap">
  <figure class="dv-card">
    <figcaption class="dv-head">
      <h2 class="dv-title">トラフィック内訳</h2>
      <p class="dv-sub">CSS conic-gradient によるドーナツ円グラフ</p>
    </figcaption>
    <div class="dv-body">
      <!-- CSS変数で各セグメントの境界角を渡す -->
      <div id="pie" class="dv-pie" role="img" aria-label="トラフィックの内訳円グラフ">
        <div class="dv-pie__hole">
          <span id="pieTotal" class="dv-pie__total">100%</span>
          <span class="dv-pie__cap">合計</span>
        </div>
      </div>
      <ul id="legend" class="dv-legend"></ul>
    </div>
  </figure>
</div>
CSS
:root {
  --dv-radius: 18px;
  --dv-ink: #f1f5f9;
  --dv-sub: #94a3b8;
  /* JSが上書きする conic-gradient 文字列の初期値 */
  --pie-gradient: conic-gradient(#334155 0turn 1turn);
  --pie-angle: 0turn; /* 全体回転で出現アニメ */
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  color: var(--dv-ink);
  background:
    radial-gradient(800px 480px at 15% 120%, #0c4a6e 0%, transparent 55%),
    linear-gradient(150deg, #0f172a, #020617);
}

.dv-wrap { width: min(92vw, 720px); padding: 20px; }

.dv-card {
  margin: 0;
  padding: 22px 24px 20px;
  border-radius: var(--dv-radius);
  background: rgba(15, 23, 42, 0.55);
  border: 1px solid rgba(148, 163, 184, 0.18);
  box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(6px);
}

.dv-head { margin-bottom: 14px; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }

.dv-body {
  display: flex;
  align-items: center;
  gap: 28px;
  flex-wrap: wrap;
  justify-content: center;
}

.dv-pie {
  --size: 156px;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  display: grid;
  place-items: center;
  /* conic-gradientで扇形を描き、回転マスクで出現させる */
  background:
    conic-gradient(from -0.25turn, transparent var(--pie-angle), #0b1220 0),
    var(--pie-gradient);
  background-blend-mode: normal;
  -webkit-mask: none;
  position: relative;
  filter: drop-shadow(0 10px 30px rgba(2, 132, 199, 0.35));
}

/* 中央の穴(ドーナツ化) */
.dv-pie__hole {
  width: 60%;
  height: 60%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 40%, #1e293b, #0b1220);
  display: grid;
  place-items: center;
  text-align: center;
  box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.15);
}

.dv-pie__total { font-size: 24px; font-weight: 700; line-height: 1; }
.dv-pie__cap { font-size: 11px; color: var(--dv-sub); margin-top: 4px; }

.dv-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
  min-width: 180px;
}

.dv-legend li {
  display: grid;
  grid-template-columns: 14px 1fr auto;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  padding: 6px 8px;
  border-radius: 8px;
  transition: background .2s ease;
  cursor: default;
}
.dv-legend li:hover { background: rgba(148, 163, 184, 0.1); }

.dv-legend .swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
}
.dv-legend .val { color: var(--dv-sub); font-variant-numeric: tabular-nums; }
JavaScript
// CSS conic-gradientの文字列を組み立ててドーナツ円グラフを描画
(() => {
  const pie = document.getElementById('pie');
  const legend = document.getElementById('legend');
  const totalEl = document.getElementById('pieTotal');
  if (!pie || !legend) return; // null安全

  // データ(値と色)。値は自動で割合に正規化する
  const data = [
    { label: 'オーガニック検索', value: 44, color: '#38bdf8' },
    { label: 'ダイレクト',       value: 26, color: '#a78bfa' },
    { label: 'SNS',             value: 18, color: '#34d399' },
    { label: 'リファラル',       value: 12, color: '#fbbf24' },
  ];
  const total = data.reduce((s, d) => s + d.value, 0);

  // conic-gradientのストップを累積角度で構築
  let acc = 0;
  const stops = data.map((d) => {
    const start = acc / total;
    acc += d.value;
    const end = acc / total;
    return `${d.color} ${start}turn ${end}turn`;
  });
  pie.style.setProperty('--pie-gradient', `conic-gradient(from -0.25turn, ${stops.join(', ')})`);

  // 凡例を生成
  const frag = document.createDocumentFragment();
  data.forEach((d) => {
    const li = document.createElement('li');
    const sw = document.createElement('span');
    sw.className = 'swatch';
    sw.style.background = d.color;
    const name = document.createElement('span');
    name.textContent = d.label;
    const val = document.createElement('span');
    val.className = 'val';
    val.textContent = `${Math.round((d.value / total) * 100)}%`;
    li.append(sw, name, val);
    frag.appendChild(li);
  });
  legend.appendChild(frag);

  if (totalEl) totalEl.textContent = '100%';

  // 回転マスクで時計回りに出現させる(reduced-motion配慮)
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduceMotion) {
    pie.style.setProperty('--pie-angle', '1turn');
    return;
  }

  const start = performance.now();
  const duration = 1100;
  function tick(now) {
    const t = Math.min(1, (now - start) / duration);
    // easeOutCubicで滑らかに
    const eased = 1 - Math.pow(1 - t, 3);
    pie.style.setProperty('--pie-angle', `${eased}turn`);
    if (t < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
})();

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

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

# 追加してほしい効果
conic円グラフ(データ可視化)
CSS conic-gradientだけで作るドーナツ円グラフ。回転マスクで出現させ、割合の内訳表示に最適です。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="dv-wrap">
  <figure class="dv-card">
    <figcaption class="dv-head">
      <h2 class="dv-title">トラフィック内訳</h2>
      <p class="dv-sub">CSS conic-gradient によるドーナツ円グラフ</p>
    </figcaption>
    <div class="dv-body">
      <!-- CSS変数で各セグメントの境界角を渡す -->
      <div id="pie" class="dv-pie" role="img" aria-label="トラフィックの内訳円グラフ">
        <div class="dv-pie__hole">
          <span id="pieTotal" class="dv-pie__total">100%</span>
          <span class="dv-pie__cap">合計</span>
        </div>
      </div>
      <ul id="legend" class="dv-legend"></ul>
    </div>
  </figure>
</div>

【CSS】
:root {
  --dv-radius: 18px;
  --dv-ink: #f1f5f9;
  --dv-sub: #94a3b8;
  /* JSが上書きする conic-gradient 文字列の初期値 */
  --pie-gradient: conic-gradient(#334155 0turn 1turn);
  --pie-angle: 0turn; /* 全体回転で出現アニメ */
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  color: var(--dv-ink);
  background:
    radial-gradient(800px 480px at 15% 120%, #0c4a6e 0%, transparent 55%),
    linear-gradient(150deg, #0f172a, #020617);
}

.dv-wrap { width: min(92vw, 720px); padding: 20px; }

.dv-card {
  margin: 0;
  padding: 22px 24px 20px;
  border-radius: var(--dv-radius);
  background: rgba(15, 23, 42, 0.55);
  border: 1px solid rgba(148, 163, 184, 0.18);
  box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(6px);
}

.dv-head { margin-bottom: 14px; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }

.dv-body {
  display: flex;
  align-items: center;
  gap: 28px;
  flex-wrap: wrap;
  justify-content: center;
}

.dv-pie {
  --size: 156px;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  display: grid;
  place-items: center;
  /* conic-gradientで扇形を描き、回転マスクで出現させる */
  background:
    conic-gradient(from -0.25turn, transparent var(--pie-angle), #0b1220 0),
    var(--pie-gradient);
  background-blend-mode: normal;
  -webkit-mask: none;
  position: relative;
  filter: drop-shadow(0 10px 30px rgba(2, 132, 199, 0.35));
}

/* 中央の穴(ドーナツ化) */
.dv-pie__hole {
  width: 60%;
  height: 60%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 40%, #1e293b, #0b1220);
  display: grid;
  place-items: center;
  text-align: center;
  box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.15);
}

.dv-pie__total { font-size: 24px; font-weight: 700; line-height: 1; }
.dv-pie__cap { font-size: 11px; color: var(--dv-sub); margin-top: 4px; }

.dv-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
  min-width: 180px;
}

.dv-legend li {
  display: grid;
  grid-template-columns: 14px 1fr auto;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  padding: 6px 8px;
  border-radius: 8px;
  transition: background .2s ease;
  cursor: default;
}
.dv-legend li:hover { background: rgba(148, 163, 184, 0.1); }

.dv-legend .swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
}
.dv-legend .val { color: var(--dv-sub); font-variant-numeric: tabular-nums; }

【JavaScript】
// CSS conic-gradientの文字列を組み立ててドーナツ円グラフを描画
(() => {
  const pie = document.getElementById('pie');
  const legend = document.getElementById('legend');
  const totalEl = document.getElementById('pieTotal');
  if (!pie || !legend) return; // null安全

  // データ(値と色)。値は自動で割合に正規化する
  const data = [
    { label: 'オーガニック検索', value: 44, color: '#38bdf8' },
    { label: 'ダイレクト',       value: 26, color: '#a78bfa' },
    { label: 'SNS',             value: 18, color: '#34d399' },
    { label: 'リファラル',       value: 12, color: '#fbbf24' },
  ];
  const total = data.reduce((s, d) => s + d.value, 0);

  // conic-gradientのストップを累積角度で構築
  let acc = 0;
  const stops = data.map((d) => {
    const start = acc / total;
    acc += d.value;
    const end = acc / total;
    return `${d.color} ${start}turn ${end}turn`;
  });
  pie.style.setProperty('--pie-gradient', `conic-gradient(from -0.25turn, ${stops.join(', ')})`);

  // 凡例を生成
  const frag = document.createDocumentFragment();
  data.forEach((d) => {
    const li = document.createElement('li');
    const sw = document.createElement('span');
    sw.className = 'swatch';
    sw.style.background = d.color;
    const name = document.createElement('span');
    name.textContent = d.label;
    const val = document.createElement('span');
    val.className = 'val';
    val.textContent = `${Math.round((d.value / total) * 100)}%`;
    li.append(sw, name, val);
    frag.appendChild(li);
  });
  legend.appendChild(frag);

  if (totalEl) totalEl.textContent = '100%';

  // 回転マスクで時計回りに出現させる(reduced-motion配慮)
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduceMotion) {
    pie.style.setProperty('--pie-angle', '1turn');
    return;
  }

  const start = performance.now();
  const duration = 1100;
  function tick(now) {
    const t = Math.min(1, (now - start) / duration);
    // easeOutCubicで滑らかに
    const eased = 1 - Math.pow(1 - t, 3);
    pie.style.setProperty('--pie-angle', `${eased}turn`);
    if (t < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
})();

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

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