惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
S
Securelist
P
Privacy & Cybersecurity Law Blog
Scott Helme
Scott Helme
T
Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
量子位
博客园 - Franky
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
T
Troy Hunt's Blog
N
News | PayPal Newsroom
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
小众软件
小众软件
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
L
Lohrmann on Cybersecurity
L
LINUX DO - 最新话题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
P
Privacy International News Feed
Last Week in AI
Last Week in AI
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
博客园_首页
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
D
DataBreaches.Net
Recent Announcements
Recent Announcements
酷 壳 – CoolShell
酷 壳 – CoolShell
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
罗磊的独立博客
Engineering at Meta
Engineering at Meta
Forbes - Security
Forbes - Security
T
Tenable Blog

Josh Collinsworth

LLMs and performative productivity Man Cereal My review of the Nüborn Baby at 3 months 2025 Year in Review AI optimism is a class privilege Alchemy Titles matter The blissful zen of a good side project Goodbye, Griff. You were a good boy. Rare words in common phrases, and how to avoid getting them wrong Things I enjoyed in 2024 The childlike and the childish A response to "Defending Open Source: Protecting the Future of WordPress" If WordPress is to survive, Matt Mullenweg must be removed For whatever it's worth: my advice on job hunting in tech A decade of code Follow-up: the Glove80 after six months The quiet, pervasive devaluation of frontend I worry our Copilot is leaving some passengers behind Things I enjoyed in 2023 First impressions of the MoErgo Glove80 ergonomic keyboard A message from the Captain of the S.S. Layoff Things you forgot (or never knew) because of React Alfred vs. Raycast: my constant debate Adding page transitions in SvelteKit Ten tips for better CSS transitions and animations Understanding easing and cubic-bezier curves in CSS Impressions of the ZSA Moonlander at one month Why you should never use px to set font-size in CSS Forty-two Breaking changes in SvelteKit, August 2022 The self-fulfilling prophecy of React Announcing Hondo Building accessible toggle buttons (with examples for Svelte, Vue, and React) Debugging iOS Safari (when all you have is a Mac) Let's learn SvelteKit by building a static Markdown blog from scratch Adding blog comments to your static site with utterances Converting from Gridsome to SvelteKit Introducing Svelte, and Comparing Svelte with React and Vue Goodbye, WordPress Announcing Quina (My First App)! How to Create Custom Editor Blocks with Block Lab A New Headless Site with Gridsome This isn't the Time, But it's the Perfect Time; Goodbye, Instagram How to Connect Local with CodeKit How to Check Uniqueness in an Array of Objects in JavaScript Adding Gutenberg Full- and Wide-Width Image Support to Your WordPress Theme Let's Learn CSS Variables! New Site, New Theme for 2018 Five Ways to Become a Better Designer (That Aren't Design) My Essential Tools for WordPress Development The Five Things I Wish Somebody Had Told Me as a Design Student WordPress Child Theme Explanation and Walkthrough Why Designers Shouldn't "Fix" Other Designers' Logos 8 Mistakes to Avoid in Your Student Design Portfolio Profit is Not a Value Understanding the Difference Between Image and Vector File Types Pantone, Color, and What I Wish I Had Known Sooner as a Designer Social Media, Compulsion, and the 12 Things I Learned on My Break from Facebook Classic rock, Mario Kart, and why we can't agree on Tailwind
Creating dynamic bar charts with CSS grid
2022-03-24 · via Josh Collinsworth

Published: March 24, 2022
Last updated: July 14, 2022

Inspired by Wordle, I recently added a bar chart element to Quina, to show players how many guesses they tend to need to finish their game. Here’s a screenshot from my phone:

A bar chart of guesses per game in Quina, shown on a mobile device

At first, when I set out to build that chart, I was concerned at the daunting complexity. How do I make sure the bars are the right scale, both in relation to each other and to the chart itself? Reaching for an external library looked like the easy option.

Then I realized: this is actually a perfect use-case for CSS grid!

Setting up the grid

The trick is actually a fairly simple one, as far as putting charts into websites go. At a high level, all we need to do is:

  • Create a grid container, which will serve as our chart, with any number of grid items (child elements) as the bars in the chart;
  • Set grid-auto-columns: 1fr on the container;
  • Set grid-column: span x on each one (where x is its value); and
  • Color in each bar.

The nifty thing about this approach is: the chart will automatically be as wide as the widest bar; no math or percentages needed!

Let’s start with a simple, albeit contrived example: a bar chart that shows how many players are on the field at a time in a sport. (Not an especially useful chart, admittedly, but it’s easily understandable data to work with.)

See the Pen Simple CSS Grid Bar Chart by Josh Collinsworth (@collinsworth) on CodePen.

If you poke around at the code a bit, you’ll see I used an unordered list (<ul>) as the chart/grid container, and made each list item (<li>) a bar in the chart. This helps to ensure an accessible experience, as the contents of each bar will be read by assistive technologies like screen readers as an individual list item (something like, “‘Baseball: 9,’ one of six”).

It may seem like a <dl> element would be a good fit here, but I consulted an accessibility expert and was told screen reader support for definition lists is not great. They recommended I use an unordered list instead in this case.

Each individual bar has a colored background to make it visible, and places the value (number) inside a <span>. That’s just so we can use display: flex and justify-content: space-between to space out the two.

Each individual bar also has grid-column: span x set on it as well, where x is the bar’s value, making each bar a column width equal to its value.

Working with dynamic data

In a real-world use case, you probably wouldn’t be hard-coding your data into the HTML. Most likely, you’d be working with data from an external source.

Reusing our example from above, that data might look something like this:

// Our array of data, as an example
[
  {
    name: 'Baseball'
    value: 9,
  }, {
    name: 'Football'
    value: 11,
  },{
    name: 'Basketball'
    value: 5,
  },
  // ...etc.
]

Using a modern framework like Svelte, we can create a component to accept that data array as a prop, process it dynamically, and display it as a chart. (If you prever Vue instead, we’ll look at a different approach a little later on.)

In Svelte, that might look something like this:

<script>
	// This component will expect the data as a prop
	export let dataPoints;
</script>

<ul class="chart">
	{#each dataPoints as point}
		<li style="grid-column: span {point.value}">
			{point.name}
			<span>{point.value}</span>
		</li>
	{/each}
</ul>

NOTE: the code above is not optimized yet! I want to explore some options, and the quirks of CSS grid just a little before we get to the final version. I’ve also omitted the styling for now.

Ok, first, this component will expect a dataPoints prop. That’s our array of (what else?) data points, as in the JavaScript example above. Each item in the array will become a bar in the chart. (We could add some defaults or type checking here to be safe, if we wanted. That would probably be a good idea in production.)

Next, we iterate over each point, using an <li> for each one, which spans as many columns as its value. (For example: if point.value is 8, then the list item will have grid-column: span 8 applied as a style, to be exactly eight columns wide on the chart.)

Again, that’s the heart of this particular CSS trick; making each bar span X columns, which ensures the chart is always exactly as wide as the widest item(s), saving us from doing any math to figure out how chart items should be sized!

This approach assumes you want the widest bar to be the width of the chart, which you might not, depending on use case. If your bar chart shows percentages, for example, it might be more intuitive to make the chart exactly 100 columns wide, so the bars are corresponding width.

In our loop, we also populate the bar’s actual visible text content with the point.name and point.value props. Again, the value is contained in a <span> element, so we can push the name and value apart from one another—though depending on your implementation and design, this might be unneeded.

I mentioned, however, that there were still some issues to solve here. Let’s get to that now.

Setting bar rows

One big problem you may run into with this approach is: grid items do not start on new rows automatically!

CSS grid tries to fit each new item onto the current row if there’s space before moving to the next row. That means you could end up with two or more of your bars butting up against each other side-by-side on the same row. You can see it happen in this case if you manually set the grid wide enough, or if the bars vary enough in size:

Setting the grid to a higher width results in some bars stacking up next to each other

That’s no good. [Morbo voice]: Charts do not work that way!

Fortunately, there’s an easy solution.

  • First, set grid-column-start: 1 on each grid item in your CSS;
  • Then, simply use grid-column-end instead of grid-column on each bar’s inline styles. (Either grid-column-end: span x;, or grid-column-end: x + 1; would work, where x is the data point’s value.)

The problem shown above happens because the only explicit direction we gave to the grid container was to make the bars span a certain number of columns—and by default, CSS grid will try to fit multiple items onto the same row where it can. But when we set both -start and -end on a grid item, the grid will place it as directed, without overlaps.

Our final Svelte version, then, might look like this:

<script>
	export let dataPoints;
</script>

<ul class="chart">
	{#each dataPoints as point}
		<li style="grid-column-end: span {point.value}">
			{point.name}
			<span>{point.value}</span>
		</li>
	{/each}
</ul>

<style>
	.chart li {
		grid-column-start: 1;
	}
</style>

CSS

There are a few important styles needed to make this work. First off, make sure your grid container has the following:

.chart {
	display: grid;
	grid-template-columns: auto; /* The default, but best to be explicit */
	gap: 0.5rem 0;
	grid-auto-columns: 1fr;
}

All of these properties are important. Without explicitly setting grid-auto-columns to 1fr, each column may not be sized the same, which would skew the appearance of our chart—and since the whole point of a chart is to show the exact relationship between items, we definitely want equal columns.

The same grid shown twice, but different. The first has 'grid-auto-columns' set to the default ('auto'), resulting in some columns being significantly wider than others. In the second, 'grid-auto-columns' is set to '1fr', ensuring each column is the same width.

I also want to call attention to the gap property. A value of 0.5rem 0 means we have some space between rows, but crucially, not between columns. You definitely want to be sure you don’t have any column gap; otherwise, the gap will be added to the width of the bars, and they could easily end up overflowing the chart. My first Quina implementation made this mistake; shout-out to Andrew Walpole (quite possibly the world’s most proficient Quina player) for finding that bug. Andrew also took this idea and ran with it in a bar chart CodePen of his own.

Next, on the bars themselves, you’ll want something like this:

.chart > li {
	display: flex;
	justify-content: space-between;
	padding: 0.25rem 0.5rem;
	background: #ffd100; /* Or your color */
	grid-column-start: 1;
}

Aside from that, the rest is pretty straightforward, and you can add or adjust depending on your design. You’ll probably want to set list-style-type to none, just to get rid of the bullets that come with a list element by default. You might want to adjust the browser’s default padding on unordered lists, as well.

One final warning: CSS wants to make elements wide enough to hold their text contents by default. So when dealing with very small bars, be sure they don’t get wider than they should in the chart, just because the name of the bar is wide.

Other options

Here I’ve shown a horizontal bar chart, but it wouldn’t take much to change the bar chart to a vertical one instead. The placement of items is just a little more tricky; here’s a fork of my pen above:

See the Pen Simple CSS Grid Bar Chart - Vertical by Josh Collinsworth (@collinsworth) on CodePen.

The above pen uses Vue instead of Svelte, if you’d like to take a peek at another framework implementation.

The main things to keep in mind if you want the bars to go vertical are:

  • The sizing of the bars will need to be switched from grid-column to grid-row.
  • When placing the bars, you’ll need to know the largest value of all the data points, in order to offset each one appropriately (because CSS grid places the item’s top at the named column then spans downward, and there’s no way to reverse that flow like you can with flexbox). In the pen above, this is accomplished with a computed property. (Alternatively, you could just choose a value you know to be higher than any bar would be and compute with that.)
    • The placement formula to make all the bars align on the bottom is: [tallest bar’s value] - [bar’s value] + 1. The extra 1 is needed because the outer edge of the chart is technically row 1.
  • You’ll still need to explicitly place each bar, but by column instead of row.
  • You might want to set a min-height on the chart, since CSS grid will only make each row as minimally tall as it needs to be in order to distribute them evenly.
  • Which reminds me: you’ll probably want to have both grid-auto-columns and grid-auto-rows set to 1fr, to make sure each bar is the same width, and that the height of each bar is properly relative.
  • You can either set height: 100% on each bar to make it span all its rows, or set align-items: stretch on the grid container. Either will work.
  • You’ll probably want to swap any spacing properties, like gap and padding, so what was the x axis is now the y axis and vice versa.
  • Finally, you’ll need to adjust the flexbox properties on the bars themselves, so that the bar label and value are pushed apart vertically rather than horizontally. (Setting flex-direction: column-reverse worked well in my case.)

The good news is: as long as you only edit the CSS and don’t touch the HTML, the chart will still be accessible!

All of this is only one possible implementation of a CSS grid bar chart. There are many other ways to do it. In fact, you could take these same principles and build other kinds of charts, as well.

You could also change the style. One possible example: experiment with using grid placement to create partially overlapping items. Animating the bars so they rise one after another might be another potential enhancement.

In any case: I hope I’ve shown you that you don’t need to reach for some chart library for simple use cases, and that you have fun trying this out!