Every article with comparative data runs into the same decision: how to show it. The automatic answer is usually to install a charting library. But for half a dozen bars, that choice is expensive — and the irony is that the result is often less accessible than the three-line CSS alternative.
This article documents the chart pattern used across this blog: horizontal bars in plain HTML and CSS, with no dependencies. Every bit of the code below is running in production right here.
The problem: 500 KB to draw four bars
Charting libraries solve real problems — computed axes, scales, interaction, time series. The catch is that you pay for the whole bundle even when you use one percent of it.
To get the real numbers, we downloaded the minified files straight from the CDN and measured the bytes, without gzip:
JavaScript weight to render one simple bar chart
Measured by Due Web Studio on 12 August 2026: minified files downloaded from jsDelivr, size in bytes without gzip compression. The "Plain HTML and CSS" row is the CSS block described in this article. Its bar is drawn at 1% for visibility — proportionally it would be 0.25%.
These are different orders of magnitude. And the cost does not stop at download: JavaScript has to be fetched, parsed and executed before it draws a single pixel, while CSS lands in the first paint. On a content page, where the chart usually sits in the middle of the text, that feeds straight into Largest Contentful Paint.
The markup: the value goes in the text, not in the bar
The most important decision in this pattern is not visual — it is where the information lives. Each chart row is a three-part grid: label, track and value.
The rule that drives everything: bar width is reinforcement of the information, never the information itself. The value appears as real text on every row.
That solves accessibility with no ARIA attributes at all. A screen reader reads "Chart.js, 204 KB" because both are text. If the CSS fails to load, the content still reads as a list. And you can select the numbers with a mouse, which a canvas chart never allows.
The structure of one row looks like this:
- A container with the chart class, wrapping everything.
- A short title in a paragraph with strong, stating what is measured and in which unit.
- One row per bar, holding label, track and value.
- Inside the track, the bar with its width as an inline percentage.
- A source paragraph at the end, citing where the data came from.
The percentage is worked out as you write: the largest value in the series becomes 100% and the rest scale against it. No arithmetic happens in the visitor's browser.
The CSS: a three-column grid and a track that clips
Each row's layout is a simple grid. The first column uses minmax() for the label — a pixel floor so it never gets squeezed, and a percentage ceiling so it never dominates the row. The middle column takes the remaining space. The third shrinks to fit the value.
Why the track needs overflow hidden
The track has rounded corners and the bar lives inside it. Without overflow: hidden on the track, a bar at 100% spills past the corners and the rounding disappears on precisely the most important bar. With it, the track clips the bar and the curve holds.
A small detail that changes the finish: the bar rounds only its right-hand corners. On the left it sits at the origin, and a rounded corner there would suggest the bar starts somewhere after zero.
The detail almost everyone forgets: tabular-nums
Numeric values stacked in a column need to align. In proportional fonts a "1" is narrower than an "8", so "527 KB" and "204 KB" end up different widths and the column wobbles. The fix is one line: font-variant-numeric: tabular-nums, which gives every digit the same advance width. It is the difference between a chart that looks designed and one that looks thrown together.
Responsive without duplicating the chart
On a narrow screen, three columns do not fit: the label gets crushed and the bar becomes a dash. The answer is not to hide anything or ship a second markup — it is to rearrange the same grid.
In a media query below 480px, the grid drops to two columns and the label spans the full width above them. The result: label on top, bar and value side by side underneath. Same HTML, same information, no conditional logic in the content — four lines of CSS.
That is the structural advantage of building it with CSS Grid rather than canvas: the chart reflows like content, because it is content.
Track contrast: the quiet mistake
The background track is usually drawn in a very light grey, almost invisible. That is a genuine accessibility problem: the guidance on non-text contrast asks for a minimum contrast on components that carry information.
Because the value lives in the text here, the track is not the only source of the information, which lowers the risk. Even so, an invisible track removes the sense of scale and makes comparing bars harder. It is worth checking contrast between track and background, and between bar and track.
When the library is genuinely worth it
It would be dishonest to end without this. The pattern above is good for one specific case: few bars, direct comparison, static data inside a piece of text. Outside that, it breaks quickly.
- Computed axes and scales: if the axis has to adapt to the data on its own, you will reimplement the library, worse.
- Real interaction: tooltips, zoom, filtering a series by clicking the legend — none of that comes out of CSS.
- Many data points: hundreds of rows of HTML weigh more than the library you avoided.
- Geometry: pie, radar, scatter and line all depend on calculation. The horizontal bar is the rare case where the geometry is just a width.
- Data that changes: if it comes from an API and updates itself, you already have JavaScript on the page anyway.
The practical test: if the chart is static, small and comparative, CSS wins comfortably. If it is interactive or dynamic, use the library and feel no guilt about it.
Key takeaways
- Put the value in the text. That covers accessibility, copy-paste and the no-CSS case in one move.
- Work out the proportion as you write, not in the visitor's browser.
- Use a three-column grid and rearrange to two on mobile — same markup.
- Do not forget overflow: hidden on the track and tabular-nums on the value.
- Drive the colours from CSS variables so the chart follows the theme.
- Know when to stop and install the library.
Conclusion
Choosing between plain CSS and a charting library is not about purism — it is about proportion between the tool and the problem. Half a dozen bars in an article does not justify half a megabyte of JavaScript, and the CSS version ends up more accessible for free, because the data starts life as text.
The same attention to weight and accessibility goes into the business websites and landing pages we build, and into the SEO and maintenance work that follows delivery. If you want to talk through your project, get in touch.
