conic円グラフ
CSS conic-gradientだけで作るドーナツ円グラフ。回転マスクで出現させ、割合の内訳表示に最適です。
ライブデモ
使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。