Tuesday, June 9, 2026

One other Stab on the Good CSS Pie Chart… Sans JavaScript!


Not too long ago, Juan Diego Rodríguez revealed a wonderful article exploring how far CSS might be pushed to construct a semantic and customizable pie chart whereas maintaining JavaScript to a minimal.

Citing Juan himself:

On this article, we’ll strive making the right pie chart in CSS. Meaning avoiding as a lot JavaScript as attainable whereas addressing main complications that comes with handwriting pie charts.

And it said some objectives that I need to undergo once more so as of precedence:

  • This have to be semantic! Which means a display screen reader ought to have the ability to perceive the info proven within the pie chart.

To my understanding, the unique article’s resolution reached that purpose. Its semantic strategy (labels in plain HTML + values as attributes reinjected into the DOM through pseudo-elements) is clear, expressive, and hopefully accessible.

  • This needs to be HTML-customizable! As soon as the CSS is completed, we solely have to alter the markup to customise the pie chart.

The unique article reached that purpose as nicely.

  • This could maintain JavaScript to a minimal! No downside with JavaScript basically, it’s simply extra enjoyable this manner.

The unique article aimed to make use of as little JavaScript as attainable, primarily for enjoyable. I are inclined to disagree barely. For me, it shouldn’t be only for enjoyable, since…

  • JavaScript is there to cope with states and logic, and
  • CSS is there to type the markup.

The preliminary “no JavaScript” constraint was significant to me. CSS needs to be highly effective sufficient to allow us to type a pie chart. JavaScript shouldn’t be required. So, I made a decision to see whether or not there was a method to 100% do away with it and, for enjoyable, forked the article’s CodePen throughout a lunch break.

I stored the unique code as unchanged as attainable, preserving its semantic strategy and HTML-side customizability. If It Ain’t Broke, Don’t Repair It™.

Coincidentally, this text got here proper after a current quick pen of mine toying with bar charts. So I used to be already within the temper for charts. However bar charts are far simpler: every bar’s place or measurement doesn’t depend upon the others. A pie chart is a unique beast: every slice’s place is determined by the earlier one. Fortunately, this made it extra of a enjoyable problem.

However earlier than diving into my tackle pie charts, let’s see how these have been approached by different internet builders.

Prior Artwork

I learn many blogs, articles, and code examples from skilled front-end builders, however I’m not one myself, so I’m not totally sure of my potential to determine essentially the most related and up-to-date prior artwork… Let’s strive anyway.

It’s straightforward to seek out many JavaScript libraries coping with charts. I’ve used them lots in my work. Nevertheless, because of our no-JavaScript constraint, we will exclude them.

I began in search of CSS-only pie charts, and one of many first libraries that pops up is Chart-CSS. It advertises semantic construction, HTML tags to show knowledge, accessibility, and uncooked knowledge contained in the markup. It appears to be an excellent library and doesn’t use any JavaScript.

As an alternative, it makes use of HTML tables, which, in my view and expertise, makes complete sense (more often than not, supply knowledge is available in a desk). Nevertheless, it doesn’t remedy the precise problem of letting the person set solely the values whereas having the beginning and finish angles of every slice mechanically computed. On this case, customers nonetheless must manually outline them.

There are additionally superb articles discussing charts or knowledge visualization basically. To call a couple of:

They simply have one small (however crucial to us) downside. Whereas these sources are priceless in explaining how chart accessibility ought to work, they don’t actually deal with straightforward HTML “interface” nor pure CSS implementations.

How I Tackled the Drawback

If you’re nonetheless studying, I assume you might be not less than considerably fascinated with my strategy. Understandably, When you simply need to see the code, right here it’s!

Initially, the explanation JavaScript was required was that every slice wanted to know the worth of the earlier one. Nevertheless, because of how CSS property inheritance works, a toddler can not know the state of one other little one. Regardless of figuring out this, I first tried to find out whether or not there have been area of interest or “voodoo” methods that will enable me to maintain the unique HTML markup and attribute-based strategy whereas eradicating JavaScript.

I do know that folks like Roman Komarov can do unbelievable issues with CSS, so I even thought-about exploring methods involving property animations. However I clearly didn’t have the time to research that path.

I returned to the core situation: due to how CSS inheritance works, youngsters can not know the state of their siblings. I clearly wanted a “surrounding entity” to deal with this.

In Juan’s put up, that “entity” was JavaScript, which might loop by means of all youngsters and compute the suitable slice accumulations.

const pieChartItems = doc.querySelectorAll(".pie-chart li");

let accum = 0;

pieChartItems.forEach((merchandise) => {
  merchandise.type.setProperty("--accum", accum);
  accum += parseFloat(merchandise.getAttribute("data-percentage"));
});

The JavaScript code units an --accum worth for every slice, which holds the proportion values of all charts previous to it. With out it, we wouldn’t know the place to place every slice and its corresponding label.

In HTML/CSS, that entity exists too: the basic mother or father ingredient. Subsequently, my resolution was to maneuver the proportion values to the mother or father.

First, let’s bear in mind what the unique markup for the pie chart seemed like this:

  • Apple
  • Banana
  • Orange
  • Strawberry

Whereas the model we’ll be utilizing appears to be like like this:

  • Apple
  • Banana
  • Orange
  • Strawberry

We’ve moved all values to the mother or father and given every merchandise a devoted title — successfully indexing them.

I had beforehand experimented with this sort of “indexing” CSS workaround, for instance, to compensate for the dearth of sibling-index() and sibling-count() features to generate random numbers. I knew this was the precise path and that the remainder would observe logically on the CSS facet.

Spoiler: sibling-index() and sibling-count() have gotten Baseline quickly!

It might appear like duplication since we didn’t add something however quite moved the attributes. Nevertheless, this slight change permits us to handle all labels and values from the mother or father in CSS. What’s finest, we nonetheless maintain all attributes shut collectively. And whilst you could say that this received’t scale as nicely, if we’ve knowledge with tons of entries, then a pie chart is never the only option to indicate it.

Optionally, we might add data-label attributes to the labels simply to pair labels and values visually.

  • Apple
  • Banana
  • Orange
  • Strawberry

Now let’s study the CSS. The implementation requires two units of some repetitive however easy CSS guidelines.

Firstly, we’ll must cross down every share to its corresponding slice. To take action, we use Juan’s and get the data-percentage attributes into CSS by means of the upgraded attr() operate. In parallel, we’ll assign them to the corresponding slice utilizing the nth-child() selector.

.pie-chart {
   /* We write one for every slice we predict we'll want */
  --p-100-1: attr(data-percentage-1 sort()); :nth-child(1) { --p-100: var(--p-100-1) }
  --p-100-2: attr(data-percentage-2 sort()); :nth-child(2) { --p-100: var(--p-100-2) }
  --p-100-3: attr(data-percentage-3 sort()); :nth-child(3) { --p-100: var(--p-100-3) }
  --p-100-4: attr(data-percentage-4 sort()); :nth-child(4) { --p-100: var(--p-100-4) }
   /*...*/
}

For that form of repetitive/incremental code, I maintain it as a one-liner with out carriage return. IMHO it’s a really acceptable exception to widespread formatting guidelines because it prevents typos by easing scan-ability of and likewise eases additional iterations (e.g., including help for extra slices). However your mileage could fluctuate.

Let’s look somewhat nearer at what’s happening right here. On the degree of the entire pie, we entry the chances for every slice by means of their index and retailer them in a corresponding CSS variable, so the fourth ingredient will get --p-100-4, the fifth ingredient will get --p-100-5, and so forth:

--p-100-4: attr(data-percentage-4 sort());

Subsequent, we cross every a --p-100 variable that’s native to every slice.

:nth-child(4) {
  --p-100: var(--p-100-4);
}

We now have all these slice values accessible at two ranges:

  • On the pie, through listed variables: --p-100-1--p-100-2--p-100-3
  • On every slice, through the --p-100 variable

Now, we’ll must calculate the corresponding --accum worth, which is the sum of the values of all earlier slices. To take action, we’ll must progressively sum every share after every slice, then assign the worth to the slice utilizing nth-child() once more.

.pie-chart {
  /* ... */
  --accum-1: 0;                                     :nth-child(1) { --accum: var(--accum-1) }
  --accum-2: calc(var(--accum-1) + var(--p-100-1)); :nth-child(2) { --accum: var(--accum-2) }
  --accum-3: calc(var(--accum-2) + var(--p-100-2)); :nth-child(3) { --accum: var(--accum-3) }
  --accum-4: calc(var(--accum-3) + var(--p-100-3)); :nth-child(4) { --accum: var(--accum-4) }
  /*...*/
}

Once more, we first work on the pie degree, the place we compute one devoted variable per slice. The primary slice is a particular case: there isn’t any earlier slice, so the buildup is 0. Whereas in the remainder, the buildup for the slice n is the buildup of the slices earlier than n−1 plus the worth of the slice n−1.

--accum-4: calc(var(--accum-3) + var(--p-100-3));

The fourth ingredient will get --accum-4, the fifth ingredient will get --accum-5, and so forth. Simply as the chances, on the degree of every slice, we assign them to the native variable --accum.

:nth-child(4) {
  --accum: var(--accum-4);
}

As soon as once more, we’ve all these slice accumulation values accessible at two ranges:

  • On the pie, through listed variables: --accum-1--accum-2--accum-3
  • On every slice, through the --accum variable

I hope future native CSS options (maybe @operate?) will forestall us from having to resort to such repetitive code. Within the meantime, this may be simplified with a CSS preprocessor (Sass, Much less).

Whereas forking the unique Pen, some questions popped out in my thoughts — that I didn’t really discover — to maintain the unique code as unchanged as attainable:

  • What about utilizing  and  for labels and values?
  • What about utilizing a 
     (since charts are sometimes extracted from tables with rows like [label, value])?

    Observe About Accessibility

    In my fork, I dealt with accessibility the identical approach Juan did, however with one slight modification: I used counter-reset / counter() as a substitute of attr() to assign the chances to the content material property. This could work simply nearly as good as attr(), however let’s ensure it's nonetheless screenreader-friendly:

    One other factor I considered altering was the label components inside every 

  • . Within the unique article, Juan makes use of a  ingredient, whereas I opted for  as a substitute. Nevertheless, I believe it might be completely acceptable to make use of the  itself. We usually consider them as being bounded inside  components, however the spec says that we might count on to make use of them in contexts “the place phrasing content material is anticipated.” So I couldn't discover any obligation to make use of them solely within the context of kinds.

    Default Colours

    Juan’s article additionally known as upon some enhancements, which I attempted to handle in my fork:

    • data-color might be omitted, and colours are then generated.
    • Colours might be outlined both on the mother or father or on the kids (person’s selection; each are supported).

    This interprets to the subsequent snippet for every slice:

    .pie-chart li {
      --color: attr(data-color sort());
      --bg-color: var(--color, hsl(calc(360deg * sibling-index() / sibling-count()) 90% 40%));
    }

    I avoided utilizing sibling-index() and sibling-count() in the principle half, since they aren’t Baseline (but, however quickly!), however I couldn’t maintain myself again since calculating the colour hue is a lot fancier with them. These features actually enable some magic!

    Nonetheless, right here is my “CSS-only polyfill”; repetitive (but easy) code:

    .pie-chart {
      :has(:nth-child(1)) { --sibling-count: 1 } :nth-child(1) { --sibling-index: 1; }
      :has(:nth-child(2)) { --sibling-count: 2 } :nth-child(2) { --sibling-index: 2; }
      :has(:nth-child(3)) { --sibling-count: 3 } :nth-child(3) { --sibling-index: 3; }
      :has(:nth-child(4)) { --sibling-count: 4 } :nth-child(4) { --sibling-index: 4; }
      /* ... */
    }

    Extra Chart Varieties?

    We now have a typical basis for different chart sorts. As a proof of idea, I carried out a bar chart mode in my fork.

    A Net Element, Maybe?

    In a approach, we have already got an online part right here — one with out JavaScript, utilizing gentle DOM.

    And to me  isn't essentially totally different that both 

     or 

    . I can see worth on this strategy when contemplating progressive enhancement, although.

    For instance, a chart that refreshes mechanically and fetches stay knowledge. However that will require JavaScript — which we're intentionally avoiding as we speak.

Related Articles

Latest Articles