I thought I had seen every way a simple chart could break, and then I took on a pro-bono project for a kids’ charity last year. They needed a landing page with data on it, a Semantic CSS Pie Chart, and I cheated. Tight deadline, so I pulled in a heavy JavaScript library for two tiny charts. It worked. It was also a sledgehammer for a nut.
In the WordPress world we trade performance for convenience all the time, but 50kb of JS for a decorative element is a bad habit. The bigger problem is that the easy solutions tend to fail on accessibility. If a screen reader cannot explain the data to a user, the chart is a broken image with extra steps.
Why conic gradients aren’t enough
Anyone who has spent time in the CSS Almanac has run into the conic-gradient() function. On paper it is ideal: one line of code gives you a pie shape, and the syntax reads well.
.gradient {
background: conic-gradient(blue 0% 12.5%, lightblue 12.5% 50%, navy 50% 100%);
}
Then the architect’s critique starts. Gradients are images, so to a screen reader that element is an empty box. Customizing one from HTML is miserable too, because the data sits hardcoded in your stylesheet. A better Semantic CSS Pie Chart starts by moving the data into the markup.
Structuring semantic HTML
The goal is data that reads correctly before the CSS even loads. I use a <figure> element wrapping an unordered list, which lets data-attributes carry the values into CSS. The browser parses the numbers while the screen reader narrates the legend.
<figure>
<figcaption>Charity Donations 2024</figcaption>
<ul class="pie-chart">
<li data-percentage="35" data-color="#ff6666"><strong>Education</strong></li>
<li data-percentage="25" data-color="#4fff66"><strong>Health</strong></li>
<li data-percentage="40" data-color="#66ffff"><strong>Food</strong></li>
</ul>
</figure>
I used the same attr() logic on a different chart type in my guide on Better CSS Bar Charts.
Trigonometric CSS functions
The labels still need placing around the circle. That used to mean absolute positioning hacks. Native CSS math functions like cos() and sin() handle it now. Give them an angle (theta) and a radius and the X and Y coordinates come straight out of the stylesheet.
.pie-chart li {
--radius: 20vmin;
--theta: calc((360deg * var(--weighing)) / 2 + var(--offset) - 90deg);
--pos-x: calc(cos(var(--theta)) * (var(--radius) + 4rem));
--pos-y: calc(sin(var(--theta)) * (var(--radius) + 4rem));
transform: translate(var(--pos-x), var(--pos-y));
}
That keeps the chart flexible. The attr() function, in the newer Chromium syntax, parses data attributes as numbers. Cross-browser support for the advanced attr() types is still patchy, so treat this as the version you grow into rather than the one you ship everywhere today.
A pragmatic touch of JavaScript
I am a pragmatist. I would take a 100% CSS solution if one existed, but calculating the accumulated offset for each slice is not possible with pure CSS variables because of the cascade. So a few lines of vanilla JS handle the offsets.
const items = document.querySelectorAll(".pie-chart li");
let accum = 0;
items.forEach((item) => {
item.style.setProperty("--accum", accum);
accum += parseFloat(item.getAttribute("data-percentage"));
});
If this pie chart work is eating your dev hours, I can take it on. I have been wrestling with WordPress since the 4.x days.
Shipping better code
Dropping the heavy library helps your Lighthouse score, and it makes the page more resilient at the same time. Semantic HTML, modern trigonometric functions and a few lines of JS get you a chart that is accessible, maintainable and light. That beats a gradient nobody on a screen reader can read.