How to Make an Area Chart With CSS

Avatar of Rami Yushuvaev
Rami Yushuvaev on (Updated on )

UGURUS offers elite coaching and mentorship for agency owners looking to grow. Start with the free Agency Accelerator today.

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.

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.

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!