You might know a few ways to create charts with pure CSS. Some of them are covered here on CSS-Tricks, and many others can be found on CodePen, but I haven’t seen many examples of “area charts” (imagine a line chart with the bottom area filled in), particularly any in HTML and CSS alone. In this article, we’ll do just that, using a semantic and accessible HTML foundation.
![A red area chart against a dark gray background.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2020/11/s_3B96DE4366834488A29D0D1C3D10A09AAB92882E189DDEECE4637A2D54E9C4CA_1605267833349_area-chart.png?resize=1200%2C600&ssl=1)
Let’s start with the HTML
To simplify things, we will be using <ul>
tags as wrappers and <li>
elements for individual data items. You can use any other HTML tag in your project, depending on your needs.
<ul class="area-chart">
<li> 40% </li>
<li> 80% </li>
<li> 60% </li>
<li> 100% </li>
<li> 30% </li>
</ul>
CSS can’t retrieve the inner HTML text, that is why we will be using CSS custom properties to pass data to our CSS. Each data item will have a --start
and an --end
custom properties.
<ul class="area-chart">
<li style="--start: 0.1; --end: 0.4;"> 40% </li>
<li style="--start: 0.4; --end: 0.8;"> 80% </li>
<li style="--start: 0.8; --end: 0.6;"> 60% </li>
<li style="--start: 0.6; --end: 1.0;"> 100% </li>
<li style="--start: 1.0; --end: 0.3;"> 30% </li>
</ul>
Here’s what we need to consider…
There are several design principles we ought to consider before moving into styling:
- Units data: We will be using unit-less data in our HTML (i.e. no
px
,em
,rem
,%
or any other unit). The--start
and--end
custom properties will be numbers between 0 and 1. - Columns width: We won’t set a fixed
width
for each<li>
element. We won’t be using%
either, as we don’t know how many items are there. Each column width will be based on the main wrapper width, divided by the total number of data items. In our case, that’s the width of the<ul>
element divided by the number of<li>
elements. - Accessibility: The values inside each
<li>
is optional and only the--start
and--end
custom properties are required. Still, it’s best to include some sort of text or value for screen readers and other assistive technologies to describe the content.
Now, let’s start styling!
Let’s start with general layout styling first. The chart wrapper element is a flex container, displaying items in a row, stretching each child element so the entire area is filled.
.area-chart {
/* Reset */
margin: 0;
padding: 0;
border: 0;
/* Dimensions */
width: 100%;
max-width: var(--chart-width, 100%);
height: var(--chart-height, 300px);
/* Layout */
display: flex;
justify-content: stretch;
align-items: stretch;
flex-direction: row;
}
If the area chart wrapper is a list, we should remove the list style to give us more styling flexibility.
ul.area-chart,
ol.area-chart {
list-style: none;
}
This code styles all of the columns in the entire chart. With bar charts it’s simple: we use background-color
and height
for each column. With area charts we are going to use the clip-path
property to set the region that should be shown.
First we set up each column:
.area-chart > * {
/* Even size items */
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
/* Color */
background: var(--color, rgba(240, 50, 50, .75));
}
To create a rectangle covering the entire area, we will reach for the clip-path
property and use its polygon()
function containing the coordinates of the area. This basically doesn’t do anything at the moment because the polygon covers everything:
.area-chart > * {
clip-path: polygon(
0% 0%, /* top left */
100% 0%, /* top right */
100% 100%, /* bottom right */
0% 100% /* bottom left */
);
}
Now for the best part!
To show just part of the column, we clip it to create that area chart-like effect. To show just the area we want, we use the --start
and --end
custom properties inside the clip-path
polygon:
.area-chart > * {
clip-path: polygon(
0% calc(100% * (1 - var(--start))),
100% calc(100% * (1 - var(--end))),
100% 100%,
0% 100%
);
}
Seriously, this one bit of CSS does all of the work. Here’s what we get:
Working with multiple datasets
Now that we know the basics, let’s create an area chart with multiple datasets. Area charts often measure more than one set of data and the effect is a layered comparison of the data.
![](https://paper-attachments.dropbox.com/s_3B96DE4366834488A29D0D1C3D10A09AAB92882E189DDEECE4637A2D54E9C4CA_1605801751041_area-chart-multiple.png)
This kind of chart requires several child elements, so we are going to replace our <ul>
approach with a <table>
.
<table class="area-chart">
<tbody>
<tr>
<td> 40% </td>
<td> 80% </td>
</tr>
<tr>
<td> 60% </td>
<td> 100% </td>
</tr>
</tbody>
</table>
Tables are accessible and search engine friendly. And if the stylesheet doesn’t load for some reason, all the data is still visible in the markup.
Again, we will use the --start
and --end
custom properties with numbers between 0 and 1.
<table class="area-chart">
<tbody>
<tr>
<td style="--start: 0; --end: 0.4;"> 40% </td>
<td style="--start: 0; --end: 0.8;"> 80% </td>
</tr>
<tr>
<td style="--start: 0.4; --end: 0.6;"> 60% </td>
<td style="--start: 0.8; --end: 1.0;"> 100% </td>
</tr>
</tbody>
</table>
So, first we will style the general layout for the wrapping element, our table, which we’ve given an .area-chart
class:
.area-chart {
/* Reset */
margin: 0;
padding: 0;
border: 0;
/* Dimensions */
width: 100%;
max-width: var(--chart-width, 600px);
height: var(--chart-height, 300px);
}
Next, we will make the <tbody>
element a flex container, displaying the <tr>
items in a row and evenly sized:
.area-chart tbody {
width: 100%;
height: var(--chart-height, 300px);
/* Layout */
display: flex;
justify-content: stretch;
align-items: stretch;
flex-direction: row;
}
.area-chart tr {
/* Even size items */
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
}
Now we need to make the <td>
elements cover each other, one element on top of each other so we get that layered effect. Each <td>
covers the entire area of the <tr>
element that contains it.
.area-chart tr {
position: relative;
}
.area-chart td {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
Let’s put the magical powers of clip-path: polygon()
to use! We’re only displaying the area between the --start
and --end
custom properties which, again, are values between 0 and 1:
.area-chart td {
clip-path: polygon(
0% calc(100% * (1 - var(--start))),
100% calc(100% * (1 - var(--end))),
100% 100%,
0% 100%
);
}
Now let’s add color to each one:
.area-chart td {
background: var(--color);
}
.area-chart td:nth-of-type(1) {
--color: rgba(240, 50, 50, 0.75);
}
.area-chart td:nth-of-type(2) {
--color: rgba(255, 180, 50, 0.75);
}
.area-chart td:nth-of-type(3) {
--color: rgba(255, 220, 90, 0.75);
}
It’s important to use colors with opacity to get a nicer effect, which is why we’re using rgba()
values. You could use hsla()
here instead, if that’s how you roll.
And, just like that:
Wrapping up
It doesn’t matter how many HTML elements we add to our chart, the flex-based layout makes sure all the items are equally sized. This way, we only need to set the width of the wrapping chart element and the items will adjust accordingly for a responsive layout.
We have covered one technique to create area charts using pure CSS. For advanced use cases, you can check out my new open source data visualization framework, ChartsCSS.org. See the Area Chart section to see how area charts can be customized with things like different orientations, axes, and even a reversed order without changing the HTML markup, and much more!
In the final CodePen, the height of the chart seems to be rendering oddly in Firefox (release 83.0). It looks fine when I view in Edge or Chrome. I popped into the CodePen and set the chart CSS to:
And then I could see the final chart.
Pen Fixed. Thank you Caleb.
This is really neat!
There is just a small typo in the seventh code block:
It reads
100% calc(100% * (1 - var(--size))),
, but it probably should bevar(--end)
instead ofvar(--size)
.Thank you for this post!
Thanks for reporting! I fixed the code block.
I really like the idea of making CSS charts from regular tables that will fall back to a usable data view if the stylesheet doesn’t load or if a necessary style property isn’t supported by the browser.
Yes, the fallback is very cool. But this technique has many more benefits – semantic structure, accessibility, search engine visibility, no JS rendering, and more…
Any way to remove the spaces between each in the list?