DEV Community

Cover image for Charts in CSS
Mads Stoumann
Mads Stoumann

Posted on Edited on

Charts in CSS

Charts have been done in CSS many times before — there's even a dedicated project. So why this post? Because CSS evolves all the time — new, cool techniques emerge, allowing us to do things simpler, or add even more complicated new features.

Let's dive in!

Markup

Like the CSS Charts-project, we'll use a <table> for our charts. It's accessible, simple and readable:

<table>
  <caption>Monthly Sales Figures ($)</caption>
  <tbody>
    <tr>
      <th scope="row">January</th>
      <td data-value="28500">$28,500</td>
    </tr>
    <tr>
      <th scope="row">February</th>
      <td data-value="22300">$22,300</td>
    </tr>
    <tr>
      <th scope="row">March</th>
      <td data-value="30100">$30,100</td>
    </tr>
<!-- etc. -->
  </tbody>
</table>
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

Notice the data-value-attribute? We'll grab that with the new, typed attr() in CSS:

--_v: attr(data-value type(<number>), 0);
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

Before we continue, let's wrap the <table> in a custom element, allowing us to add a few custom attributes, as well as functioning as a container, we can use to make responsive charts (more on that later!):

<data-chart
  aria-label="Annual Revenue Growth"
  max="50000"
  min="0"
  role="figure">
  <table> ... </table>
</data-chart>
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

We'll grab the min and max attributes with attr() as well, setting fallbacks to 0 and 100:

  --_min: attr(min type(<number>), 0);
  --_max: attr(max type(<number>), 100);
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

Next, we'll add a simple grid to the <table>, setting fixed heights for the caption and the label-area, and 1fr for the chart itself:

Simple Grid

A bit of math

The first chart we'll build is a Column Chart. For that, we need to calculate the height of each column:

td {
  height: calc(
  ((var(--_v) - var(--_min)) / 
  (var(--_max) - var(--_min))) 
  * 100cqb - var(--data-chart-label-h)
  );
}
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

So what's going on? We calculate the height based on min and max and the current value, then multiply by 100cqb, which is the height of the "container" — finally, we deduct the height of the labels.

We'll skip the logic for colors (see final demo) — let's just add the <tbody>-part and see what we have so far:

Table Data Added

Cool! Now, let's add some grid lines and numeric labels along the y-axis. Because it's a <table>, we can't just add any tag. Since we're not using <thead>, let's use that:

<thead aria-hidden="true">
  <tr>
    <th colspan="2">50000</th>
  </tr>
  <tr>
    <th colspan="2"></th>
  </tr>
  <tr>
    <th colspan="2">40000</th>
  </tr>
  <!-- etc. -->
</thead>
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

We need aria-hidden, so it's not picked up by screen readers.

This is styled like this:

Thead Added

Let's see how <thead> and <tbody> look together without Dev Tool's grid inspector:

Final Column Chart

Nice! Now, how about we create some variants?

Variants

The variants will use the exact same markup, but because of some limitations, we have to add a few extra attributes (for now).

Area Chart

For an Area Chart, we can re-use most of the Column Chart; we just need the value of the previous element for each table cell. Unfortunately, we can't grab the previous value with:

:nth-of-type(calc(sibling-index() - 1))
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

We could create a bunch of :has(tr:nth-of-type(x))-selectors to grab the value of the previous cell, but that would make the code a bit messy.

So — for now — we set the previous value on each <td> as data-prev:

<tr>
  <th scope="row">January</th>
  <td data-value="28500" data-prev="28500">$28,500</td>
</tr>
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

Next, we create a simple polygon clip-path that creates a diagonal line from the previous value to the current value:

clip-path: polygon(-1% 100%,
  -1% calc(var(--_py) * 100%),
  101% calc(var(--_y) * 100%),
  101% 100%);
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

Let's not dive too deep into this, it's simply y-coordinates, calculated from value and previous value.

Area Chart

OK, this is one of the cases where a single color looks better. Let's update --data-chart-bar-bg:

Area Chart Single Color

Line Chart

A line chart is almost the same as an area chart. We simply need to cut off the bottom part as well. That distance is the height of the line, which we define in --line-chart-line-h:

clip-path: polygon(-1% calc(var(--_py) * 100% + var(--line-chart-line-h) / 2),
  -1% calc(var(--_py) * 100% - var(--line-chart-line-h) / 2),
  101% calc(var(--_y) * 100% - var(--line-chart-line-h) / 2),
  101% calc(var(--_y) * 100% + var(--line-chart-line-h) / 2));
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

Now we get:

Line Chart

Bar Chart

The Bar Chart does not need the data-prev-attribute, but it does require a few style-changes. Let's not dive deep into these changes — instead, inspect the styles in the final demo.

Bar Chart

Pie Chart

The Pie Chart is a bit different! Here, we need to use conic-gradients, where each gradient needs to start where the previous one ends. Thus, we need an accumulated value (--_av) and a total of all values (--_t):

tbody {
  --_t: attr(data-t type(<number>), 0);
}
td {
  --_av: attr(data-av type(<number>), 0);
  --_start: calc(
    (var(--_av) / var(--_t)) * 
    360deg
  );
  --_end: calc(
    (var(--_v) / var(--_t)) * 
    360deg
  );

  background: conic-gradient(
    from var(--_start),
    var(--_bg) 0 var(--_end),
    #0000 var(--_end) 
    calc(var(--_start) + 360deg)
  );
}
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

This gives us:

Pie Chart

I'd really like it if we could use CSS counters to do the accumulated value and total, but couldn't get it to work (maybe someone like Temani Afif can!?).

Donut Chart

A donut chart is the same as a pie chart, but with a "cut out", for which we use a simple CSS Mask:

tbody {
  mask: radial-gradient(circle, #0000 40%, #000 40%);
}
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

... and voilà, we get:

Donut Chart

Grouped Charts

Grouped charts are simply done with multiple cells per table row:

<tr>
  <th scope="row">January</th>
  <td data-value="12500">12500</td>
  <td data-value="9800">9800</td>
  <td data-value="6200">6200</td>
</tr>
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

In the styles we then group them — and in some cases, present them like individual "layers". Inspect the styles in the final demo for more details — here's how they look:

Grouped Column Chart

Multi Column Column Chart

Grouped Area Chart

Multi Column Area Chart

Grouped Line Chart

Multi Column Line Chart

Grouped Bar Chart

Multi Column Chart

Grouped Donut Chart

Multi Column Donut Chart


Responsive Charts

One cool thing about controlling charts in CSS, is how easily we can make them responsive. First, let's add two new attributes to our <data-chart>-component:

<data-chart
  items-sm="7"
  items-xs="4"
>
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

These indicate how many columns we want to show per breakpoint, in this case "sm" (small) and "xs" (x-small). Add as many as you want, and control the breakpoint in a @container-query:

@container (max-width: 400px) {
  &:is([type="area"], [type="column"], [type="line"]) {
    &[items-xs="1"] tbody tr:nth-of-type(n+2),
    &[items-xs="2"] tbody tr:nth-of-type(n+3) {
      display: none;
    }
  }
}
📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung 📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung

So — in this case — if items-sm="1", hide all items after n+2 etc. Not rocket science, and hopefully something we can make more beautiful in the future.

Let's resize the screen and check it out. First, "sm":

Small

Next, "xs":

XS


Demos

I've created a bunch of demos, all forked from the first, as they all share the same CSS.

The first is shown below. Go to this collection to see them all.

NOTE: As the responsiveness is controlled by the size of the container, you'll most likely see the "sm"-version of the charts. Open CodePen in full screen to play around and resize. Also, note that Safari and Firefox do not support typed attr(), and thus the respnsiveness only works with a JS fallback.

Top comments (10)

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
dotallio profile image
Dotallio •

Really cool how you’re taking advantage of the latest CSS features for charts, especially keeping things accessible and responsive. Which chart type do you find most challenging to get just right in practice?

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thank you! The charts themselves were not tricky, but I would have liked to be able to use CSS to get the previous values in the area/line charts!

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
artydev profile image
artydev •

The GOAT :-)

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann • • Edited

Thank you! 🐐

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
tebani profile image
tebani •

Very nice! Is there a way to get the values into the Pie and Donut Graph?

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

For these you need the accumulated and total, as mentioned in the article. I’ll publish a web component soon, so you just feed it with JSON.

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
dannyengelman profile image
Danny Engelman • • Edited

There is a <pie-chart> Web Component: pie-meister.github.io/

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung Thread
 
madsstoumann profile image
Mads Stoumann • • Edited

I’ll make <data-chart>, where you can set a type-attribute to either: area, bar, column, donut, line or pie

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
chovy profile image
chovy •

this is awesome but can you do a realtime candlestick chart?

📺 Info Siaran: madsstoumann/charts in css di - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Some comments may only be visible to logged-in visitors. Sign in to view all comments.