Building CSS bar charts with sibling-index() and attr()

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.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.