CSS Pie Chart
A pie chart drawn with conic-gradient(), with no SVG and no JavaScript.
A CSS pie chart is a single conic-gradient() painted on a square element that has been rounded off with border-radius: 50%. Every slice is a pair of numbers in the gradient's stop list, and the whole chart is one declaration. There is no SVG, no canvas and no chart library, which makes it the cheapest chart on any page that only needs to show four or five proportions.
The demo shows a plain pie and a donut built from the same gradient with a hole punched through the middle. Two more builds follow: a version where the slice sizes live in custom properties and calc() does the running totals, and a segmented chart with gaps between the wedges. For a ring that animates as it draws, see the animated donut chart.
Pie and donut from one gradient
Pie chart
Donut variant
HTML
<div class="chart-stack">
<div class="chart-wrap">
<div>
<div class="pie pie-simple"></div>
<p class="chart-title">Pie chart</p>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-dot" style="background: #ff6b6b"></span>
<span class="legend-label">Category A</span>
<span class="legend-val">35%</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background: #38bdf8"></span>
<span class="legend-label">Category B</span>
<span class="legend-val">25%</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background: #b8ff57"></span>
<span class="legend-label">Category C</span>
<span class="legend-val">20%</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background: #c084fc"></span>
<span class="legend-label">Category D</span>
<span class="legend-val">20%</span>
</div>
</div>
</div>
<div class="chart-wrap">
<div>
<div class="pie pie-donut"></div>
<p class="chart-title">Donut variant</p>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-dot" style="background: #b8ff57"></span>
<span class="legend-label">Revenue</span>
<span class="legend-val">42%</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background: #38bdf8"></span>
<span class="legend-label">Cost</span>
<span class="legend-val">23%</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background: #ff6b6b"></span>
<span class="legend-label">Tax</span>
<span class="legend-val">20%</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background: #ffd444"></span>
<span class="legend-label">Other</span>
<span class="legend-val">15%</span>
</div>
</div>
</div>
</div>
CSS
.chart-stack {
display: flex;
flex-direction: column;
gap: 1rem;
}
.chart-wrap {
display: flex;
gap: 2rem;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.pie {
width: 160px;
height: 160px;
border-radius: 50%;
flex-shrink: 0;
}
/* Each stop pair is one slice, measured in percent of the sweep */
.pie-simple {
background: conic-gradient(
#ff6b6b 0% 35%,
#38bdf8 35% 60%,
#b8ff57 60% 80%,
#c084fc 80% 100%
);
}
/* Donut variant: punch the middle out with a radial gradient on top */
.pie-donut {
background:
radial-gradient(circle at center, #0c0c0d 50%, transparent 50%),
conic-gradient(
#b8ff57 0% 42%,
#38bdf8 42% 65%,
#ff6b6b 65% 85%,
#ffd444 85% 100%
);
}
.legend {
display: flex;
flex-direction: column;
gap: .5rem;
}
.legend-item {
display: flex;
align-items: center;
gap: .5rem;
font-size: .82rem;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-label {
color: #88888f;
}
.legend-val {
margin-left: auto;
font-weight: 600;
color: #f0f0f0;
min-width: 2.5rem;
text-align: right;
}
.chart-title {
text-align: center;
font-size: .8rem;
color: #88888f;
margin: .5rem 0 0;
font-weight: 500;
}
Other ways to build it
Slice sizes as data, running totals from calc
Both charts below use exactly the same rule. The only difference between them is four numbers set inline as custom properties, and the gradient works out the cumulative stops itself. That matters more than it sounds: with hand written totals, changing the first slice from 42 to 30 means editing six other numbers, and getting one of them wrong produces a chart that still renders and is quietly incorrect. Here the raw values sit where a template or a build step can write them, and the arithmetic cannot drift out of step with the data.
42 / 28 / 18 / 12
15 / 12 / 38 / 35
25 / 25 / 25 / 25
HTML
<div class="pc-data" style="--a: 42; --b: 28; --c: 18; --d: 12"></div>
<div class="pc-data" style="--a: 15; --b: 12; --c: 38; --d: 35"></div>
CSS
.pc-data {
width: 160px;
height: 160px;
border-radius: 50%;
flex-shrink: 0;
/* four equal slices unless the markup says otherwise */
--a: 25;
--b: 25;
--c: 25;
--d: 25;
/* each stop is the running total of everything before it, so the
numbers above stay the raw data rather than a set of positions */
background: conic-gradient(
#ff6b6b 0
calc(var(--a) * 1%),
#38bdf8 calc(var(--a) * 1%)
calc((var(--a) + var(--b)) * 1%),
#b8ff57 calc((var(--a) + var(--b)) * 1%)
calc((var(--a) + var(--b) + var(--c)) * 1%),
#c084fc calc((var(--a) + var(--b) + var(--c)) * 1%)
100%
);
}
Separated wedges, and the same stops as a donut
A gap between slices is another pair of stops in the same sweep, filled with whatever color sits behind the chart rather than left empty, since a conic gradient has nothing behind it to show through. Note that the color here is the panel these demos sit in rather than the page background, which is the point: neither the gaps nor the donut hole are transparent, and both break the moment the surface under them changes. Each wedge gives up 1.2% of the turn to the gap that follows it, which is fine for a chart meant to be read and wrong for one meant to be measured, since a 35% slice is now drawn at 33.8%.
Separated wedges
Same stops, hole punched
HTML
<div class="chart-wrap">
<div>
<div class="pie pc-seg"></div>
<p class="chart-title">Separated wedges</p>
</div>
<div>
<div class="pie pc-seg-donut"></div>
<p class="chart-title">Same stops, hole punched</p>
</div>
</div>
CSS
/* 1.2% of the turn goes to each gap, so a 35% slice is drawn at 33.8% */
.pc-seg {
width: 160px;
height: 160px;
border-radius: 50%;
background: conic-gradient(
#ff6b6b 0 33.8%,
#141415 33.8% 35%,
#38bdf8 35% 58.8%,
#141415 58.8% 60%,
#b8ff57 60% 78.8%,
#141415 78.8% 80%,
#c084fc 80% 98.8%,
#141415 98.8% 100%
);
}
/* the identical stop list with a disc of the page color on top */
.pc-seg-donut {
width: 160px;
height: 160px;
border-radius: 50%;
background:
radial-gradient(circle at center, #141415 52%, transparent 52%),
conic-gradient(
#ff6b6b 0 33.8%,
#141415 33.8% 35%,
#38bdf8 35% 58.8%,
#141415 58.8% 60%,
#b8ff57 60% 78.8%,
#141415 78.8% 80%,
#c084fc 80% 98.8%,
#141415 98.8% 100%
);
}
How it works
conic-gradient() sweeps a gradient around a center point rather than along a straight line, which is the shape a pie chart needs. Each color stop defines a slice: conic-gradient(red 0% 35%, blue 35% 60%) gives a red wedge from 0% to 35% of the sweep and a blue one from 35% to 60%. border-radius: 50% turns the square element into the circle. The donut variant layers a radial-gradient over the middle through the multi layer background shorthand.
A conic gradient starts at 12 o'clock and runs clockwise, and its stop positions can be given as angles or as percentages of the full turn. The important piece of syntax is the double position: #ff6b6b 0% 35% sets the same color at both 0% and 35%, which produces a hard edge instead of a blend. Give each color a single position and you get a smooth rotation from one hue to the next, which is a color wheel rather than a chart. Almost every broken looking CSS pie chart is a missing second position.
Stops are cumulative, not relative. Slices of 35, 25, 20 and 20 have to be written as 0 to 35, 35 to 60, 60 to 80 and 80 to 100, so every value after the first is a running total rather than the size of that slice. Doing that arithmetic by hand is where the mistakes come from: leave a gap between one slice's end and the next one's start and the browser blends across it, and overlap them and the later color wins. The first variant below hands the addition to calc() so the CSS holds the raw numbers.
border-radius: 50% is the only thing making the chart round. The gradient itself paints the entire square, corners included, so removing the radius leaves a perfectly valid square chart with wedges radiating from the middle. That is occasionally what you want, and it is worth knowing that the shape and the data are entirely independent here.
The donut hole is not a hole. radial-gradient(circle at center, #0c0c0d 50%, transparent 50%) is a disc of the page's background color painted on top of the conic gradient as a second background layer, which works only because the surface behind it is a known flat color. Put the same chart on a photograph or a gradient and the middle shows as a dark circle. A real hole needs mask-image with the same radial gradient, or an SVG ring with a stroke, which is the approach the donut chart page takes.
The limits are worth stating plainly. A conic gradient has no notion of a slice as an object, so there is nothing to hover, nothing to click and nothing to label from CSS alone. It also cannot animate the sweep smoothly, because a percentage inside a gradient is not an interpolable value the way a length is; animating it needs @property to register the custom property with a type. If you need interaction or a growing animation, use SVG. If you need to render a static proportion in as little code as possible, this is unbeatable.
CSS properties used
conic-gradient()- Sweeps color around a center point from 12 o'clock clockwise. Each stop takes two positions to produce a hard slice edge rather than a blend.
border-radius50%makes the square element a circle. The gradient paints the whole box regardless, so the radius is purely the shape.radial-gradient()- With a hard stop at 50% it paints an opaque disc over the center, which reads as a donut hole on a known flat background.
background- Takes multiple comma separated layers, first one on top. That layering is what lets the hole sit over the conic gradient in one declaration.
custom properties- Holding each slice as a plain number keeps the raw data in one place and lets
calc()build the cumulative stops. calc()- Turns slice sizes into running totals inside the gradient, so changing one slice does not mean re-adding every stop after it.
Browser support
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
conic-gradient() | 69 | 83 | 12.1 | 79 |
border-radius | 4 | 3 | 3.1 | 12 |
custom properties | 49 | 31 | 10 | 16 |
calc() | 19 | 4 | 6 | 12 |
multiple backgrounds | 4 | 3.6 | 3.1 | 12 |
Figures come from Can I Use. Firefox was the last to ship conic-gradient() and did so in version 83, which is the number that matters here: everything else in the chart has been supported far longer. In a browser without it the background declaration is dropped entirely and the element renders with no background at all, so if you need a fallback, declare a flat background color before the gradient rather than after it.
Accessibility notes
A gradient is invisible to assistive technology. There is no text, no role and no value in a conic-gradient(), so a screen reader announces an empty element. The legend beside the chart is what carries the information, and it should be real markup with the label and the number as text. A <dl> or a small table is better than a row of styled <span> elements if the numbers matter.
Color is the only thing separating the slices, which fails for anyone with a color vision deficiency and fails completely in a monochrome print. Put the percentage in the legend next to each label, order the legend to match the order of the slices, and pick colors that differ in lightness as well as hue so the wedges stay distinguishable when the hue information is gone.
Pie charts are hard to read past about five slices, and the difficulty is in the encoding rather than the implementation: people compare angles poorly and lengths well. If the data has eight categories, or if the point of the chart is a comparison rather than a share of a whole, a bar chart will communicate it better with the same amount of CSS.
What you can build with it
- Budget and spend breakdowns. Four or five categories summing to a whole, which is the case a pie is genuinely good at.
- Storage and quota indicators. Used against free, where a two slice pie or a donut makes the ratio readable at a glance in a small space.
- Survey results. Response distributions in a report, where the chart is static and printed rather than interactive.
- Progress rings. A single slice against a track color, which is a two stop conic gradient and the cheapest progress indicator there is.
- Dashboard tiles. Small multiples across a grid, where one shared rule and one custom property per tile keeps the CSS from multiplying with the data.
Mistakes worth avoiding
- Giving each color one stop position instead of two. The browser blends between them and the chart comes out as a smooth color wheel with no visible slice boundaries.
- Writing slice sizes instead of running totals.
red 0% 35%, blue 0% 25%puts blue back at the start and paints over red, which reads as the first slice having vanished. - Using a non-square element. The gradient stays circular but
border-radius: 50%makes an ellipse, so the wedges are stretched along one axis and the proportions read wrong. - Assuming the donut hole is transparent. It is a disc of a hard coded color, so the chart breaks the moment the surface behind it changes. Use
mask-imagewhen the background is not a known flat color. - Trying to transition the stop positions. Gradient percentages are not interpolated by default, so the chart jumps between states instead of sweeping. Registering the value with
@propertyis the only way to get a smooth change.
Frequently asked questions
How do you make a pie chart with only CSS?
conic-gradient() on a square element and set border-radius: 50%. Give every color two stop positions, a start and an end, expressed as running totals of the slice sizes. Four slices is four pairs of numbers in one declaration.Why does my conic-gradient look like a rainbow?
#38bdf8 35% 60%, and each color will fill its wedge with a hard edge at both ends.How do I turn a CSS pie chart into a donut?
radial-gradient as the first background layer with a hard stop, such as radial-gradient(circle at center, #0c0c0d 50%, transparent 50%). That paints a disc of the background color over the middle. If the surface behind it is not flat, use the same gradient as a mask-image instead so the middle is genuinely cut out.Can a CSS pie chart animate?
@property with a <percentage> syntax makes it animatable. An SVG arc with stroke-dashoffset is the more portable route.