Talking of charts… When was the final time you had to make use of a pie chart? In case you are a type of individuals who have to present displays proper and left, then congratulations! You’re each in my private hell… and in addition surrounded by pie charts. Fortunately, I believe I haven’t wanted to make use of them in ages, or a minimum of that was till lately.
Final 12 months, I volunteered to make ta webpage for a youngsters’ charity in México1. Every thing was fairly normal, however the employees wished some knowledge displayed as pie charts on their touchdown web page. They didn’t give us loads of time, so I admit I took the straightforward route and used one in all the various JavaScript libraries on the market for making charts.
It seemed good, however deep down I felt soiled; pulling in a complete library for a few easy pie charts. Appears like the straightforward approach out moderately than crafting an actual resolution.
I wish to amend that. On this article, we’ll attempt making the right pie chart in CSS. Meaning avoiding as a lot JavaScript as potential whereas addressing main complications that comes with handwriting pie charts. However first, let’s set some objectives that our “excellent” ought to adjust to.
So as of precedence:
- This have to be semantic! Which means a display reader ought to be capable to perceive the information proven within the pie chart.
- This ought to be HTML-customizable! As soon as the CSS is finished, we solely have to alter the markup to customise the pie chart.
- This could hold JavaScript to a minimal! No drawback with JavaScript on the whole, it’s simply extra enjoyable this fashion.
As soon as we’re achieved, we should always get a pie chart like this one:

Is that this an excessive amount of to ask? Possibly, however we’ll attempt it anyhow.
Conic gradients suck aren’t the most effective
We will’t speak about pie charts with out speaking first about conic gradients. In case you’ve learn something associated to the conic-gradient() operate, then you definitely’ve doubtless seen that they can be utilized to create easy pie charts in CSS. Heck, even I’ve stated so in the almanac entry. Why not? If solely with one factor and a single line of CSS…
.gradient {
background: conic-gradient(blue 0% 12.5%, lightblue 12.5% 50%, navy 50% 100%);
}
We will have seemlessly excellent pie chart:
Nevertheless, this methodology blatantly breaks our first purpose of semantic pie charts. Because it’s later famous on the identical entry:
Don’t use the
conic-gradient()operate to create an actual pie chart, or some other infographics for that matter. They don’t maintain any semantic that means and will solely be used decoratively.
Do not forget that gradients are photographs, so displaying a gradient as a background-image doesn’t inform display readers something concerning the pie charts themselves; they solely see an empty factor.
This additionally breaks our second rule of constructing pie charts HTML-customizable, since for every pie chart we’d have to alter its corresponding CSS.
So ought to we ditch conic-gradient() altogether? As a lot as I’d prefer to, its syntax is just too good to go so let’s a minimum of attempt to up its shortcomings and see the place that takes us.
Enhancing semantics
The primary and most dramatic drawback with conic-gradient() is its semantics. We would like a wealthy markup with all the information laid out so it may be understood by display readers. I need to admit I don’t know one of the best ways to semantically write that, however after testing with NVDA, I imagine it is a adequate markup for the duty:
<determine>
<figcaption>Candies offered final month</figcaption>
<ul class="pie-chart">
<li data-percentage="35" data-color="#ff6666"><sturdy>Candies</sturdy></li>
<li data-percentage="25" data-color="#4fff66"><sturdy>Gummies</sturdy></li>
<li data-percentage="25" data-color="#66ffff"><sturdy>Laborious Sweet</sturdy></li>
<li data-percentage="15" data-color="#b366ff"><sturdy>Bubble Gum</sturdy></li>
</ul>
</determine>
Ideally, that is all we want for our pie chart, and as soon as kinds are achieved, simply enhancing the data-* attributes or including new <li> components ought to replace our pie chart.
Only one factor although: In its present state, the data-percentage attribute received’t be learn out loud by display readers, so we’ll should append it to the tip of every merchandise as a pseudo-element. Simply bear in mind so as to add the “%” on the finish so it additionally will get learn:
.pie-chart li::after {
content material: attr(data-percentage) "%";
}
So, is it accessible? It’s, a minimum of when testing in NVDA. Right here it’s in Home windows:
You will have some questions concerning why I selected this or that. In case you belief me, let’s hold going, but when not, right here is my thought course of:
Why use data-attributes as a substitute of writing every share instantly?
We might simply write them inside every <li>, however utilizing attributes we will get every share on CSS by means of the attr() operate. And as we’ll see later it makes working with CSS a complete lot simpler.
Why <determine>?
The <determine> factor can be utilized as a self-contained wrapper for our pie chart, and apart from photographs, it’s used loads for diagrams too. It is useful since we can provide it a title inside <figcaption> after which write out the information on an unordered record, which I didn’t know was among the many content material permitted inside <determine> since <ul> is taken into account circulation content material.
Why not use ARIA attributes?
We might have used an aria-description attribute so display readers can learn the corresponding share for every merchandise, which is arguably an important half. Nevertheless, we might have to visually present the legend, too. Meaning there isn’t any benefit to having percentages each semantically and visually since they could get learn twice: (1) as soon as on the aria-description and (2) once more on the pseudo-element.
Making it a pie chart
We now have our knowledge on paper. Now it’s time to make it seem like an precise pie chart. My first thought was, “This ought to be straightforward, with the markup achieved, we will now use a conic-gradient()!”
Nicely… I used to be very mistaken, however not due to semantics, however how the CSS Cascade works.
Let’s peek once more on the conic-gradient() syntax. If we have now the next knowledge:
- Merchandise 1: 15%
- Merchandise 2: 35%
- Merchandise 3: 50%
…then we’d write down the next conic-gradient():
.gradient {
background:
conic-gradient(
blue 0% 15%,
lightblue 15% 50%,
navy 50% 100%
);
}
This mainly says: “Paint the primary colour from 0 to fifteen%, the subsequent colour from 15% to 50% (so the distinction is 35%), and so forth.”
Do you see the difficulty? The pie chart is drawn in a single conic-gradient(), which equals a single factor. It’s possible you’ll not see it, however that’s horrible! If we wish to present every merchandise’s weight inside data-percentage — making every little thing prettier — then we would wish a technique to entry all these percentages from the mother or father factor. That’s unimaginable!
The one approach we will get away with the simplicity of data-percentage is that if every merchandise attracts its personal slice. This doesn’t imply, nonetheless, that we will’t use conic-gradient(), however moderately we’ll have to make use of a couple of.
The plan is for every of these things to have their very own conic-gradient() portray their slice after which place all of them on prime of one another:

To do that, we’ll first give every <li> some dimensions. As an alternative of hardcoding a measurement, we’ll outline a --radius property that’ll turn out to be useful later for holding our kinds maintainable when updating the HTML.
.pie-chart li {
--radius: 20vmin;
width: calc(var(--radius) * 2); /* radius twice = diameter */
aspect-ratio: 1;
border-radius: 50%;
}
Then, we’ll get the data-percentage attribute into CSS utilizing attr() and its new kind syntax that permits us to parse attributes as one thing apart from a string. Simply beware that the brand new syntax is at the moment restricted to Chromium as I’m penning this.
Nevertheless, in CSS it is much better to work with decimals (like 0.1) as a substitute of percentages (like 10%) as a result of we will multiply them by different models. So we’ll parse the data-percentage attribute as a <quantity> after which divide it by 100 to get our share in decimal kind.
.pie-chart li {
/* ... */
--weighing: calc(attr(data-percentage kind(<quantity>)) / 100);
}
We nonetheless want it as a share, which implies multiplying that consequence by 1%.
.pie-chart li {
/* ... */
--percentage: calc(attr(data-percentage kind(<quantity>)) * 1%);
}
Lastly, we’ll get the data-color attribute from the HTML utilizing attr() once more, however with the <colour> kind this time as a substitute of a <quantity>:
.pie-chart li {
/* ... */
--bg-color: attr(data-color kind(<colour>));
}
Let’s put the --weighing variable apart for now and use our different two variables to create the conic-gradient() slices. These ought to go from 0% to the specified share, after which develop into clear afterwards:
.pie-chart li {
/* ... */
background: conic-gradient(
var(--bg-color) 0% var(--percentage),
clear var(--percentage) 100%
);
}
I’m defining the beginning 0% and ending 100% explicitly, however since these are the default values, we might technically take away them.
Right here’s the place we’re at:
Maybe a picture will assist in case your browser lacks help for the brand new attr() syntax:

Now that every one the slices are achieved, you’ll discover every of them begins from the highest and goes in a clockwise route. We have to place these, you understand, in a pie form, so our subsequent step is to rotate them appropriately to kind a circle.
That is once we hit an issue: the quantity every slice rotates will depend on the variety of gadgets that precede it. We’ll should rotate an merchandise by no matter measurement the slice earlier than it’s. It might be best to have an accumulator variable (like --accum) that holds the sum of the odds earlier than every merchandise. Nevertheless, because of the approach the CSS Cascade works, we will neither share state between siblings nor replace the variable on every sibling.
And imagine me, I attempted actually laborious to work round these points. Nevertheless it appears we’re compelled into two choices:
- Hardcode the
--accumvariable on every<li>factor. - Use JavaScript to calculate the
--accumvariable.
The selection isn’t that tough if we revisit our objectives: hardcoding --accum would negate versatile HTML since transferring an merchandise or altering percentages would pressure us to manually calculate the --accum variable once more.
JavaScript, nonetheless, makes this a trivial effort:
const pieChartItems = doc.querySelectorAll(".pie-chart li");
let accum = 0;
pieChartItems.forEach((merchandise) =>; {
merchandise.type.setProperty("--accum", accum);
accum += parseFloat(merchandise.getAttribute("data-percentage"));
});
With --accum out of the best way, we will rotate every conic-gradient() utilizing the from syntax, that tells the conic gradient the rotation’s place to begin. The factor is that it solely takes an angle, not a share. (I really feel like a share must also work superb, however that’s a subject for an additional time).
To work round this, we’ll should create one more variable — let’s name it --offset — that is the same as --accum transformed to an angle. That approach, we will plug the worth into every conic-gradient():
.pie-chart li {
/* ... */
--offset: calc(360deg * var(--accum) / 100);
background: conic-gradient(
from var(--offset),
var(--bg-color) 0% var(--percentage),
clear var(--percentage) 100%
);
}
We’re trying loads higher!

What’s left is to put all gadgets on prime of one another. There are many methods to do that, after all, although the simplest is likely to be CSS Grid.
.pie-chart {
show: grid;
place-items: middle;
}
.pie-chart li {
/* ... */
grid-row: 1;
grid-column: 1;
}
This little little bit of CSS arranges the entire slices within the lifeless middle of the .pie-chart container, the place every slice covers the container’s solely row and column. They slices received’t collide as a result of they’re correctly rotated!

Apart from these overlapping labels, we’re in actually, actually good condition! Let’s clear that stuff up.
Positioning labels
Proper now, the title and share labels contained in the <figcaption> are splattered on prime of each other. We would like them floating subsequent to their respective slices. To repair this, let’s begin by transferring all these gadgets to the middle of the .pie-chart container utilizing the identical grid-centering trick we we utilized on the container itself:
.pie-chart li {
/* ... */
show: grid;
place-items: middle;
}
.pie-chart li::after,
sturdy {
grid-row: 1;
grid-column: 1;
}
Fortunately, I’ve already explored how one can lay issues out in a circle utilizing the newer CSS cos() and sin(). Give these hyperlinks a learn as a result of there’s loads of context in there. In brief, given an angle and a radius, we will use cos() and sin() to get the X and Y coordinates for every merchandise round a circle.
For that, we’ll want — you guessed it! — one other CSS variable representing the angle (we’ll name it --theta) the place we’ll place every label. We will calculate that angle this subsequent components:
.pie-chart li {
/* ... */
--theta: calc((360deg * var(--weighing)) / 2 + var(--offset) - 90deg);
}
It’s price understanding what that components is doing:
360deg * var(--weighing)) / 2: Will get the share as an angle then divides it by two to search out the center level.+ var(--offset): Strikes the angle to match the present offset.- 90deg.cos()andsin(): The angles are measured from the appropriate, howeverconic-gradient()begins from the highest. This half corrects every angle by-90deg.
We will discover the X and Y coordinates utilizing the --theta and --radius variables, like the next pseudo code:
x = cos(theta) * radius
y = sin(theta) * radius
Which interprets to…
.pie-chart li {
/* ... */
--pos-x: calc(cos(var(--theta)) * var(--radius));
--pos-y: calc(sin(var(--theta)) * var(--radius));
}
This locations every merchandise on the pie chart’s edge, so we’ll add in a --gap between them:
.pie-chart li {
/* ... */
--gap: 4rem;
--pos-x: calc(cos(var(--theta)) * (var(--radius) + var(--gap)));
--pos-y: calc(sin(var(--theta)) * (var(--radius) + var(--gap)));
}
And we’ll translate every label by --pos-x and --pos-y:
.pie-chart li::after,
sturdy {
/* ... */
remodel: translateX(var(--pos-x)) translateY(var(--pos-y));
}
Oh wait, only one extra minor element. The label and share for every merchandise are nonetheless stacked on prime of one another. Fortunately, fixing it’s as straightforward as translating the share a bit extra on the Y-axis:
.pie-chart li::after {
--pos-y: calc(sin(var(--theta)) * (var(--radius) + var(--gap)) + 1lh);
}
Now we’re cooking with gasoline!

Let’s make certain that is screenreader-friendly:
That’s about it… for now…
I’d name this a very good begin towards a “excellent” pie chart, however there are nonetheless a number of issues we might enhance:
- The pie chart assumes you’ll write the odds your self, however there ought to be a technique to enter the uncooked variety of gadgets after which calculate their percentages.
- The
data-colorattribute is okay, but when it isn’t supplied, we should always nonetheless present a technique to let CSS generate the colours. Maybe an excellent job forcolor-mix()? - What about various kinds of charts? Bar charts, anybody?
- That is sorta screaming for a pleasant hover impact, like possibly scaling a slice and revealing it?
That’s all I might give you for now, however I’m already planning to chip away at these at comply with up with one other piece (get it?!). Additionally, nothing is ideal with out a lot of suggestions, so let me know what you’d change or add to this pie chart so it may be actually excellent!
1 They’re nice folks serving to youngsters by means of extraordinarily tough occasions, so in case you are keen on donating, you’ll find extra on their socials. ↪️

