conic-gradient() draws color around a center point, which makes it a perfect tool for pie charts and single slices. By the end of this article you will build a flexible pie and donut chart powered by custom properties, plus a reusable “slice” component for tooltips, badges, or loaders. You will understand angles, color stops, and masking, and you will know how to animate conic sweeps without adding extra DOM.
Why conic-gradient() Matters
Pie charts used to require SVG arcs or a handful of stacked elements with transforms. conic-gradient() makes circular partitions trivial with a single background, which reduces markup and keeps styling in one place. You can bind data to CSS custom properties and express each segment in percentages, which reads cleanly and scales to many segments. The result is light, themeable, and maintainable, and you can switch between a filled pie or a donut by dropping in a mask gradient.
Prerequisites
You only need a small amount of CSS knowledge. We will use variables and pseudo-elements plus a touch of masking. Basic familiarity with percentages and degrees helps, but the examples do the math for you.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The chart is a single box for the conic-gradient background, followed by a semantic list for the legend. Values are embedded as CSS custom properties on the chart itself, so you can update data inline or from a build step. The slice component is a standalone element you can reuse anywhere.
<!-- HTML -->
<div class="chart" role="img" aria-label="Revenue breakdown: Product A 40%, Product B 25%, Services 20%, Other 15%">
<div class="pie pie--donut" style="--a:40; --b:25; --c:20; --d:15"></div>
<ul class="legend" aria-hidden="false">
<li><span class="swatch sw-a"></span> Product A <b>40%</b></li>
<li><span class="swatch sw-b"></span> Product B <b>25%</b></li>
<li><span class="swatch sw-c"></span> Services <b>20%</b></li>
<li><span class="swatch sw-d"></span> Other <b>15%</b></li>
</ul>
</div>
<!-- Standalone slice demo -->
<div class="slice" style="--size:25%; --start:135deg; --color: hsl(12 85% 55%)"></div>
Step 2: The Basic CSS & Styling
The base styles set up colors, chart size, and a pleasant background. The variables in :root make it easy to theme, and the .pie block computes cumulative stops from percentages so you can define segments as 40, 25, 20, 15 without writing calc chains inline.
/* CSS */
:root {
--c1: hsl(12 85% 55%);
--c2: hsl(199 85% 55%);
--c3: hsl(142 50% 40%);
--c4: hsl(48 90% 50%);
--bg: #0e1116;
--text: #e6e9ef;
--muted: #96a0aa;
}
*,
*::before,
*::after { box-sizing: border-box; }
body {
margin: 0;
min-height: 100svh;
display: grid;
place-content: center;
color: var(--text);
background:
radial-gradient(80rem 80rem at 10% -20%, #1a2230, transparent),
radial-gradient(80rem 80rem at 110% 120%, #111826, transparent),
var(--bg);
font: 500 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, "Apple Color Emoji", "Segoe UI Emoji";
}
.chart {
--size: 260px;
display: grid;
justify-items: center;
gap: 1rem;
}
.pie {
width: var(--size);
height: var(--size);
border-radius: 50%;
display: grid;
place-items: center;
/* cumulative stops from percentages set inline */
--s1: var(--a);
--s2: calc(var(--a) + var(--b));
--s3: calc(var(--a) + var(--b) + var(--c));
/* start at the top (12 o'clock) by rotating -90deg */
background:
conic-gradient(from -90deg,
var(--c1) 0% var(--s1)%,
var(--c2) var(--s1)% var(--s2)%,
var(--c3) var(--s2)% var(--s3)%,
var(--c4) var(--s3)% 100%
);
}
/* donut mask: cut a hole from the middle */
.pie--donut {
-webkit-mask:
radial-gradient(closest-side, transparent 58%, #fff 59%);
mask:
radial-gradient(closest-side, transparent 58%, #fff 59%);
}
/* legend */
.legend {
display: grid;
grid-template-columns: repeat(2, max-content);
gap: .5rem 1.5rem;
padding: 0;
margin: 0;
list-style: none;
color: var(--text);
}
.legend li { display: flex; align-items: center; gap: .5rem; }
.legend b { color: var(--muted); font-weight: 600; }
.swatch {
inline-size: 1rem;
block-size: 1rem;
border-radius: .25rem;
display: inline-block;
}
.sw-a { background: var(--c1); }
.sw-b { background: var(--c2); }
.sw-c { background: var(--c3); }
.sw-d { background: var(--c4); }
/* standalone slice */
.slice {
--size: 25%;
--start: 0deg;
--color: var(--c1);
inline-size: 140px;
block-size: 140px;
border-radius: 50%;
background:
conic-gradient(from calc(var(--start) - 90deg),
var(--color) 0 var(--size),
transparent var(--size) 1turn
);
/* make it a ring to match donut style */
-webkit-mask:
radial-gradient(closest-side, transparent 58%, #fff 59%);
mask:
radial-gradient(closest-side, transparent 58%, #fff 59%);
box-shadow: 0 0 0 1px #0008 inset, 0 2px 24px #0006;
margin-inline: auto;
}
Advanced Tip: Use percentages for conic stops to match your data directly. If you already know how to make a circle with CSS, think of conic-gradient as painting that circle by portion. A 25% segment maps to 90deg without any extra math in your head.
Step 3: Building the Pie Chart with conic-gradient()
The pie is a single element with a conic-gradient background. We layer color stops in order, using cumulative percentages to define start and end for each slice. The from -90deg offset moves the zero angle to the top so your chart reads like most dashboard pies.
/* CSS */
.pie {
width: var(--size);
height: var(--size);
border-radius: 50%;
--s1: var(--a);
--s2: calc(var(--a) + var(--b));
--s3: calc(var(--a) + var(--b) + var(--c));
background:
conic-gradient(from -90deg,
var(--c1) 0% var(--s1)%,
var(--c2) var(--s1)% var(--s2)%,
var(--c3) var(--s2)% var(--s3)%,
var(--c4) var(--s3)% 100%
);
}
How This Works (Code Breakdown)
conic-gradient() paints clockwise around the center. If you omit stops, it blends between colors; for charts we want hard edges, so each color gets a start and end. The variables –s1, –s2, and –s3 are cumulative sums of the inline percentages. That keeps the markup lean and avoids repeating calc expressions in each stop.
The default conic start is at 0deg on the right. Most charts start at the top, so from -90deg rotates the whole gradient to 12 oclock. You can change this to match your design. A small negative or positive angle nudges where the first slice begins, which helps align a label or a guide line. When you need a half or quarter partition, stick to percentages like 50% or 25%; it aligns with related shapes such as a quarter circle. If you already know how to make a quarter circle top right with CSS, the 25% arc maps one-to-one to the conic stop 0 25%.
To flip between a full pie and a donut, you do not need extra elements. The mask gradients in .pie–donut carve a hole using the alpha mask. White keeps pixels, transparent discards. The result is a ring that still uses the same conic background. The same trick builds spinners, loaders, and croissants, which connects nicely with the technique used to make a Pac‑Man shape with CSS.
Step 4: Building the Single Slice Component
A slice is just a conic wedge from a start angle for a given size. This component is handy for legends, tooltips, hover previews, and progress arcs. The mask turns it into a ring so it visually matches the donut chart.
/* CSS */
.slice {
--size: 20%; /* arc length (percentage of the circle) */
--start: 0deg; /* where the arc begins */
--color: hsl(210 80% 55%);
inline-size: 140px;
block-size: 140px;
border-radius: 50%;
background:
conic-gradient(from calc(var(--start) - 90deg),
var(--color) 0 var(--size),
transparent var(--size) 1turn
);
-webkit-mask:
radial-gradient(closest-side, transparent 58%, #fff 59%);
mask:
radial-gradient(closest-side, transparent 58%, #fff 59%);
}
How This Works (Code Breakdown)
The slice uses two conic color bands: one painted segment up to var(–size), and transparent for the remainder of the turn. The from calc(var(–start) – 90deg) parameter positions the wedge so you can align it with a particular category on the main chart. Because the second band is transparent, the element stays a circle but only the wedge is visible.
The radial mask turns the circle into a ring by erasing the center. If your chart uses a different inner radius, change 58% and 59% to your preferred thickness. Because the mask is independent from the conic background, you can reuse this slice for different ring widths without changing the wedge logic.
Angles and percentages are interchangeable in conic-gradient stops. 25% equals 90deg equals 0.25turn. Using percentages reads best when you are mapping to data. If you prefer degrees for readability while debugging, set –size: 90deg and it will render the same wedge.
Advanced Techniques: Adding Animations & Hover Effects
A sweep-in reveal helps users read the chart order and sets focus on the composition. Rather than animating each stop, animate a conic mask that grows from 0turn to 1turn. This keeps the gradient static and only changes the mask, which is both smooth and easy to control. You can also add a subtle hover glow or expand the ring thickness on the slice component for emphasis.
/* CSS */
/* Animatable custom property for the sweep angle */
@property --reveal {
syntax: "<angle>";
inherits: false;
initial-value: 0turn;
}
/* sweep-in reveal using a conic mask layered with the donut mask */
.pie.reveal {
animation: sweep 1200ms cubic-bezier(.2,.7,0,1) forwards;
-webkit-mask:
conic-gradient(from -90deg, #fff 0 var(--reveal), transparent 0),
radial-gradient(closest-side, transparent 58%, #fff 59%);
mask:
conic-gradient(from -90deg, #fff 0 var(--reveal), transparent 0),
radial-gradient(closest-side, transparent 58%, #fff 59%);
}
@keyframes sweep { to { --reveal: 1turn; } }
/* reduce motion */
@media (prefers-reduced-motion: reduce) {
.pie.reveal { animation: none; --reveal: 1turn; }
}
/* hover accent for the slice component */
.slice:hover {
box-shadow:
0 0 0 2px color-mix(in oklab, var(--color), white 20%) inset,
0 10px 30px color-mix(in oklab, var(--color), black 70%);
-webkit-mask:
radial-gradient(closest-side, transparent 55%, #fff 56%); /* thicker ring on hover */
mask:
radial-gradient(closest-side, transparent 55%, #fff 56%);
}
Accessibility & Performance
Accessibility
Treat the chart as an image and give it a descriptive aria-label that states the category values in plain language. The legend provides a text alternative as well. If the chart is purely decorative, set aria-hidden=”true” on the chart container. Avoid relying on color alone; include percentages or labels next to color swatches so users with color-vision deficiency can read the data. For motion, respect prefers-reduced-motion by disabling the sweep animation and starting with the full mask value.
Performance
conic-gradient() is rasterized by the browser efficiently and animates well when you move it with transforms or reveal it with masks. Refrain from animating many color stops directly; animating a mask angle or a transform usually costs less. Keep DOM small by using one element per chart and a single gradient; that beats stacking elements with rotations. CSS masks are GPU friendly on modern engines. If your chart updates frequently, changing a handful of custom properties is cheap and keeps layout stable.
Build Charts that Pull Their Weight
You built a complete pie and donut chart with conic-gradient(), plus a reusable slice component that slots into legends or detail views. You learned how to control angles, map percentages to stops, and carve rings with masks, all while keeping markup brief. Now you have a pattern you can extend with tooltips, labels, and multi-series rings without leaving CSS.