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.
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;
}
<div class="chart"></div>
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;
}
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 thecontent
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>
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%);
}
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));
}
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, then
is not useable in thecalc()
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 }
:::
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%);
}
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)
Quite helpful for those who don't want to integrate dependency !
Good tutorial, thanks!
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-...
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?
That is definitely possible. You can wrap this code in a React/Vue/Svelte component for instance and populate everything.
thanks, I was thinking of a plain javascript version. Maybe some of your viewers have some sample code? I am new to bar charts.
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:
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:
P.S.: It would be much easier in React heheh
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