DEV Community

Cover image for Creating a stacked bar chart using only CSS
Kevin Pennekamp
Kevin Pennekamp

Posted on • Updated on • Originally published at crinkles.dev

Creating a stacked bar chart using only CSS

In various projects, I always seem to struggle with responsive charts. These libraries generate charts in SVGs, often with fixed dimensions or ratios. This means that different screen sizes either get additional whitespace, or parts of the chart get hidden. Horrible. So I gave myself a challenge to create a responsive CSS-only bar chart, the one visualized below.

GIF with the expected result

The base of the chart

Alright, let’s first make the graph itself. The graph is nothing more than multiple bars aligned horizontally. Or, you know, in a row. As we want all the bars to stick to the bottom, we need to set align-items: flex-end. Floating bars from the top look cool, but in the end, add little value to most charts. The gap is needed to tell each of the bars apart.



.chart {
    display: flex;
    flex-direction: row;
    align-items: flex-end;
    gap: 2px;
}


Enter fullscreen mode Exit fullscreen mode


<div class="chart"></div>


Enter fullscreen mode Exit fullscreen mode

Now we can start defining our bars. Each of the bars is a vertical stack of sections. So, a flexbox with the direction column would suffice. With the flex-grow: 1 we ensure the bars fill up all the available horizontal space equally. As you can see in the example, we do expect that a bar that is being hovered gets more space. This allows us to display values with the bar the user is (kinda) interacting with.



.bar {
    display: flex;
    flex-direction: column;
    flex-grow: 1;
}

.bar:hover {
    flex-grow: 6;
}


Enter fullscreen mode Exit fullscreen mode

The size of the bars

Now we only need to determine the height of each of the bars. Ideally, I would have liked to use a data-* attribute. Reading values in CSS from these attributes can be done using the attr() function, as it only works with string values, and. But unfortunately, that will not work.

The attr() function can only work with string values. This means it only has value for the content attribute in CSS.

The only way I could make it work is by adjusting custom properties via the style attribute on the HTML element, like shown below. It’s not a solution I prefer, but for our use case, it does work. And combined with modern JavaScript frameworks it is often hidden for developers in custom UI components.



<div class="chart">
    <div class="bar" style="--bar-ratio: 68%;"></div>
</div>


Enter fullscreen mode Exit fullscreen mode

As you can see in both the HTML snippet above and the CSS snippet below, we are working with percentages. To have the chart scale nicely, we need to give the bar with the highest value a height of 100%, and all others scale according to their values.



.bar {
    height: var(--bar-ratio, 0%);
}


Enter fullscreen mode Exit fullscreen mode

Stacking the bars

As we are looking at a stacked bar chart, we need to add sections to each of the bars. We already know that a bar is set up as a vertical flexbox. To ensure each section fills up the space of the bar corresponding to their value. If we have three sections with values 10, 20, and 30, we can achieve the result to set flex-grow to this value. In summary, flex-grow: var(--value). Like with the height of the bar, we need to inject the value through the style=“--value: 30;” tag.

If the value is small compared to the other sections, other CSS attributes, such as padding, might impact the correct distribution.



.section {
    display: flex;
    flex-grow: var(--value);
}

.section:hover {
    flex-grow: calc(10 * var(--value));
}


Enter fullscreen mode Exit fullscreen mode

From a user experience perspective, we want to highlight the section on interactivity, i.e. hover. By simply expanding the flex-grow, just like with the bar, we get the effect that we want. Both the bar within the entire chart, as the section in the bar is expanding in size on hover.

Improve the experience

Stacked bar charts visualize different series of data. Each nth section of a bar belongs to the same series of data. This means we need to have a way to indicate that they belong to each other. In most libraries, you can define a set of colors. But I wanted a more CSS-only solution. I already deviated from this by setting values through the style attribute. So I want to avoid more deviation.

A nifty little trick that I learned is setting a --nth-child custom property in the root of your styling, as shown below. This makes it possible to use these values with the calc() function.

You might think using :nth-child(n) would allow you to achieve the same, without all the custom properties. Unfortunately, the n is not useable in the calc() function.



:nth-child(1) { --nth-child: 1 }
:nth-child(2) { --nth-child: 2 }
:nth-child(3) { --nth-child: 3 }
:nth-child(4) { --nth-child: 4 }
:nth-child(5) { --nth-child: 5 }


Enter fullscreen mode Exit fullscreen mode

:::
If you have different types of children within a parent, you can use :nth-of-type. This does target HTML tags. If you only use <div /> it will make no difference.
:::

Now we have a variable that we can use to indicate the index of an element. We can use math to visualize sections of the same series. Examples are background-color or opacity. Let’s go for background-color. The easiest way would be to use the hsl() function and change the degrees of the colors, as shown below. As there are 256 degrees, taking a base of 100 gives us at least six different colors, before colors (almost) start looking the same.



.section {
background-color: hsl(calc(100 * var(--nth-child)) 100% 40%);
}

Enter fullscreen mode Exit fullscreen mode




Wrapping up

By combining different forms of flexbox, :hover and some small tricks, we can create a nice responsive bar chart. The only downside is you need to bind the values via the style attribute in the HTML templates. This should not be an issue in most modern frameworks. But, it is still something to be aware of. Curious about the live example? Then visit this codepen.io link.

Top comments (8)

Collapse
 
suman373_30 profile image
Suman Roy

Quite helpful for those who don't want to integrate dependency !

Collapse
 
udanielnogueira profile image
Daniel Nogueira

Good tutorial, thanks!

Collapse
 
rickdelpo1 profile image
Rick Delpo

I finally got around to reformatting and populating this bar chart.

This post uses moment.js for grouping by months
dev.to/rickdelpo1/stacked-bar-char...

This post ditches moment.js and I use array.reduce to group my months
dev.to/rickdelpo1/how-to-populate-...

Collapse
 
rickdelpo1 profile image
Rick Delpo

Nice Post! I like the idea of not using a JS chart library. Can we add a Javascript Fetch to populate the stacked bars from a JSON datasource?

Collapse
 
vyckes profile image
Kevin Pennekamp

That is definitely possible. You can wrap this code in a React/Vue/Svelte component for instance and populate everything.

Collapse
 
rickdelpo1 profile image
Rick Delpo

thanks, I was thinking of a plain javascript version. Maybe some of your viewers have some sample code? I am new to bar charts.

Thread Thread
 
leandro_n_ortiz profile image
Leandro Ortiz • Edited

I don't have a sample code, but with plain javascript, you can use fetch to perform API calls and get the values you need.
Then, you just need to pass it to the "style" property to work with the example of this post.
For "fetch" documentation:
developer.mozilla.org/en-US/docs/W...

It's not usual to develop an application with API calls without any framework or rendering lib, but it would be something like this:

fetch('https://jsonplaceholder.typicode.com/todos/')
 .then(response => response.json())
 .then(list => {
    list.forEach(item => {
      document.getElementById(`todo-${item.id}`).style = `--bar-ratio: ${item.id}%;`
    });
  });
Enter fullscreen mode Exit fullscreen mode

I couldn't find a mock server that return a useful number to populate the chart, so I used the "id" of the todo list as the value...sorry

Then the HTML would be:

<div class="chart">
    <div id="todo-1" class="bar" style=""></div>
    <div id="todo-2" class="bar" style=""></div>
    <div id="todo-3" class="bar" style=""></div>
    <div id="todo-4" class="bar" style=""></div>
    <div id="todo-5" class="bar" style=""></div>
</div>
Enter fullscreen mode Exit fullscreen mode

P.S.: It would be much easier in React heheh

Thread Thread
 
rickdelpo1 profile image
Rick Delpo

Hey thanks very much for the response. When I get a chance I will update my code and post here. It is not a high priority right now but I have a use case that I want to implement over the next few months. I really want to stay with Plain JS vs React