I have lost more hours than I want to admit to chart libraries that bloat the frontend. It usually starts with a client asking for a “simple” dashboard, and somehow that means 150KB of JavaScript to draw four vertical bars. CSS Grid got us off canvas-based charts years ago, but we were still passing values in through inline styles or hand-written nth-child selectors. Two newer CSS functions, sibling-index() and the upgraded attr(), take away that last bit of glue in a CSS bar chart.
The standard advice for data visualization has been “just use a library” for years. Open one up, though, and a lot of what it does is work the browser now handles on its own. I made a similar point in my breakdown of modern CSS layouts: the less translation you need between your data and your styles, the less code you have to maintain. These functions cut out most of that translation.
Starting with a semantic list
A chart is a list of numbers, so write it as one. A pile of <div> tags gives a screen reader nothing to work with. An unordered list with ARIA roles keeps the data readable no matter what the CSS does to it visually.
<ul class="bbioon-chart" role="list" aria-label="Monthly Revenue">
<li class="chart-bar" data-value="32" role="img" aria-label="32 percent">32%</li>
<li class="chart-bar" data-value="46" role="img" aria-label="46 percent">46%</li>
<li class="chart-bar" data-value="85" role="img" aria-label="85 percent">85%</li>
</ul>
The CSS functions behind the bar chart
sibling-index() gives each bar its own column without a wall of .bar:nth-child(3) { grid-column: 3; } rules, which accounts for most of the size difference in the stylesheet. attr() is the bigger change: it now takes a type parameter such as number, so the browser reads a data attribute as a real CSS value and you can feed it straight into a property like grid-row-end.
.bbioon-chart {
display: grid;
grid-template-rows: repeat(100, 1fr);
gap: 10px;
height: 300px;
align-items: end;
}
.chart-bar {
/* Automatically places the bar in the correct column */
grid-column: sibling-index();
/* Spans the number of rows defined in the data-attribute */
grid-row: span attr(data-value number);
background: var(--wp--preset--color--primary);
list-style: none;
}
One catch: sibling-index() support is still rolling out, and Firefox is the holdout. On a public site you still want a fallback or a post-processor. On internal tools and modern-only builds, this is the version of the code I would actually ship.
Rendering the chart in WordPress
In a custom block or a dashboard widget the values are never known ahead of time, so they cannot be hardcoded. I let PHP prepare and sanitize the data and keep the markup dumb, the same split I used for the hexagon grids last month.
<?php
/**
* Render a dynamic chart using modern CSS functions.
*
* @param array $dataset Array of labels and values.
*/
function bbioon_render_dynamic_chart( $dataset ) {
if ( empty( $dataset ) ) return '';
$output = '<ul class="bbioon-chart" role="list">';
foreach ( $dataset as $data ) {
$val = intval( $data['value'] );
$label = esc_attr( $data['label'] );
$output .= sprintf(
'<li class="chart-bar" data-value="%d" role="img" aria-label="%s: %d percent">%d%%</li>',
$val,
$label,
$val,
$val
);
}
$output .= '</ul>';
return $output;
}
If chart work like this is eating your dev hours, I take on that kind of job. I have been wrestling with WordPress since the 4.x days.
What you gain by dropping the library
On a lot of sites the bottleneck is not the server, it is what the browser has to run once the page arrives. Native attr() casting and sibling-index() take the external library out of that equation, and what is left is a stylesheet you can read in one sitting. For four bars on a dashboard, that trade is easy.