Skip to content

ExpressiveCSS

Browse documentation

Column chart

Values compared across categories or periods, drawn as columns from a data table, grouped or stacked.

View as Markdown

Put a <table> of values in a <figure class="column-chart"> to draw them as columns that grow from zero. The first column of the table labels each group. Each further column is a series named by its header cell.

It takes the same markup as a line chart. JavaScript draws the chart from the table and hides the table visually, so a screen reader still reads the data as a table. Without JavaScript, the table shows. A band and a tooltip follow the pointer, and the arrow keys move them when the chart has focus.

Monthly revenue, 2026
MonthRevenue
Jan$4,400
Feb$3,400
Mar$3,700
Apr$4,900
May$4,600
Jun$5,000
Jul$5,500
Aug$4,000
Sep$4,500
Oct$5,000
Nov$4,000
Dec$5,500
<figure class="column-chart">
  <figcaption>Monthly revenue, 2026</figcaption>
  <table>
    <thead>
      <tr><th>Month</th><th>Revenue</th></tr>
    </thead>
    <tbody>
      <tr><th>Jan</th><td>$4,400</td></tr>
      <tr><th>Feb</th><td>$3,400</td></tr>
      <tr><th>Mar</th><td>$3,700</td></tr>
      <!-- One row per month -->
    </tbody>
  </table>
</figure>

The tooltip shows each cell's text as written, so format values in the table.

Several series

Each value column after the first adds a column to every group, and a legend appears. Series take primary, tertiary, secondary and on-surface-variant in turn. Keep a grouped chart to two or three series. Past that, the columns get too thin to compare. Add toggle to the figure to make each legend item a button that shows or hides its series, as on the line chart.

Subscriptions by plan, first half
MonthBasicPremiumEnterprise
Jan5,5003,5003,200
Feb6,4003,2003,700
Mar7,6003,4002,700
Apr5,9004,0003,100
May5,8004,3004,500
Jun7,6004,3004,100
<figure class="column-chart">
  <figcaption>Subscriptions by plan, first half</figcaption>
  <table>
    <thead>
      <tr><th>Month</th><th>Basic</th><th>Premium</th><th>Enterprise</th></tr>
    </thead>
    <tbody>
      <tr><th>Jan</th><td>5,500</td><td>3,500</td><td>3,200</td></tr>
      <!-- One row per month -->
    </tbody>
  </table>
</figure>

Ranges

Give a series header the class range and each of its cells two numbers, such as 120–480. The column floats from the lower to the higher, rounded at both ends. Write the ends with an en dash, a spaced hyphen or "to", or put them in data-value="120 480". Any other cell is a gap. The tooltip shows the cell as written, and other series draw from zero beside the range as usual. A bar chart reads ranges the same way.

Response time (ms), fastest to slowest and median
DayFastest to slowestMedian
Mon120–480210
Tue95–390180
Wed140–620260
Thu110–450200
Fri160–710310
<figure class="column-chart axis">
  <figcaption>Response time (ms), fastest to slowest and median</figcaption>
  <table>
    <thead>
      <tr><th>Day</th><th class="range">Fastest to slowest</th><th>Median</th></tr>
    </thead>
    <tbody>
      <tr><th>Mon</th><td>120–480</td><td>210</td></tr>
      <!-- … -->
    </tbody>
  </table>
</figure>

Stacked

Add stacked to draw one column per group, with each series on top of the ones before it, so the column's height is the total. Only the top segment rounds its end. The tooltip still shows each cell. Stack values that add up and keep them positive. An empty cell leaves the total unknown, so the chart leaves out the segments above it.

Subscriptions by plan, stacked
MonthBasicPremiumEnterprise
Jan5,5003,5003,200
Feb6,4003,2003,700
Mar7,6003,4002,700
Apr5,9004,0003,100
May5,8004,3004,500
Jun7,6004,3004,100
<figure class="column-chart stacked">
  <figcaption>Subscriptions by plan, stacked</figcaption>
  <table><!-- … --></table>
</figure>

Fade and track

Add fade to fade each column toward its base. Add track to draw a full-height track behind each group and round both ends of its columns. Narrow the columns with --md-comp-column-chart-column-width to make them bars on a rail.

Revenue, first half
MonthRevenue
Jan$4,400
Feb$3,400
Mar$3,700
Apr$4,900
May$4,600
Jun$5,000
Revenue, second half
MonthRevenue
Jul$5,500
Aug$4,000
Sep$4,500
Oct$5,000
Nov$4,000
Dec$5,500
<figure class="column-chart fade">
  <figcaption>Revenue, first half</figcaption>
  <table><!-- … --></table>
</figure>

<figure class="column-chart track" style="
    --md-comp-column-chart-column-width: 24%;
    --md-comp-column-chart-shape: 8px;
    --md-comp-column-chart-grid-color: transparent">
  <figcaption>Revenue, second half</figcaption>
  <table><!-- … --></table>
</figure>

Sparkline

sparkline makes a 48px chart with no grid, labels, legend or tooltip. Use it for a count beside a figure. Hide its caption with visually-hidden when the tile already names it.

Signups this week
353
+12% from last week
Signups per day this week
DaySignups
Mon42
Tue38
Wed51
Thu47
Fri56
Sat61
Sun58
<dl class="stats">
  <div>
    <dt>Signups this week</dt>
    <dd>353</dd>
    <dd class="stat-change up">+12% from last week</dd>
    <dd>
      <figure class="column-chart sparkline my-0">
        <figcaption class="visually-hidden">Signups per day this week</figcaption>
        <table><!-- … --></table>
      </figure>
    </dd>
  </div>
</dl>

Chart cards

Put a chart in a card with the figures it explains, and put the period switch beside the stat. When the period changes:

  1. Update the stat.
  2. Swap the table rows.
  3. Call update() to redraw. If the figure has data-watch, skip the call.

The card adds no chart styles, so size a chart with its tokens. A card pads only its headings and paragraphs, so wrap the stat and chart in <div class="p-4"> to keep them off the border. A connected button group fills its container, so give it a <div> of its own to keep it beside the stat.

Revenue
$54,500
34.9% profit margin
Expenses and profit per month
MonthExpensesProfit
Jan$2,500$1,500
Feb$2,200$1,200
Mar$2,500$1,600
Apr$1,900$1,300
May$2,300$1,400
Jun$2,500$1,500
Jul$2,300$1,200
Aug$2,100$1,100
Sep$2,400$2,000
Oct$2,500$2,100
Nov$2,200$1,800
Dec$2,600$2,300
<article class="outlined" id="profit-card">
  <div class="p-4">
    <div class="flex flex-wrap justify-between align-start gap-3">
      <dl class="stats my-0">
        <div>
          <dt>Revenue</dt>
          <dd>$54,500</dd>
          <dd class="stat-change">34.9% profit margin</dd>
        </div>
      </dl>
      <div>
        <div class="button-group connected xsmall" data-selection="single"
             data-selection-required role="group" aria-label="Period">
          <button type="button" class="button tonal" aria-pressed="true"
                  data-period="12" data-total="$54,500"
                  data-change="34.9% profit margin">Year</button>
          <button type="button" class="button tonal" aria-pressed="false"
                  data-period="6" data-total="$28,500"
                  data-change="36.8% profit margin">6 months</button>
        </div>
      </div>
    </div>
    <figure class="column-chart stacked">
      <figcaption class="visually-hidden">Expenses and profit per month</figcaption>
      <table><!-- … --></table>
    </figure>
  </div>
</article>

<script>
  const card = document.getElementById('profit-card');
  const figure = card.querySelector('.column-chart');
  const body = figure.querySelector('tbody');
  const rows = [...body.rows];
  card.querySelectorAll('[data-period]').forEach((button) => {
    button.addEventListener('click', () => {
      body.replaceChildren(...rows.slice(-Number(button.dataset.period)));
      card.querySelector('.stats dd').textContent = button.dataset.total;
      card.querySelector('.stat-change').textContent = button.dataset.change;
      Expressive.ColumnChart.getInstance(figure).update();
    });
  });
</script>

For a compact card, set the height, widen the columns and clear the grid. A sparkline with flex-1 beside the figures fills the rest of a short tile.

Signups this week
353
+12% from last week
Signups per day this week
DaySignups
Mon42
Tue38
Wed51
Thu47
Fri56
Sat61
Sun58
Revenue
$54,500
+6.2% from last year
Monthly revenue, 2026
MonthRevenue
Jan$4,400
Feb$3,400
Mar$3,700
Apr$4,900
May$4,600
Jun$5,000
Jul$5,500
Aug$4,000
Sep$4,500
Oct$5,000
Nov$4,000
Dec$5,500
<article class="outlined">
  <div class="p-4">
    <dl class="stats my-0">…</dl>
    <figure class="column-chart" style="
        --md-comp-column-chart-height: 120px;
        --md-comp-column-chart-column-width: 80%;
        --md-comp-column-chart-grid-color: transparent">
      <figcaption class="visually-hidden">Signups per day this week</figcaption>
      <table><!-- … --></table>
    </figure>
  </div>
</article>

<article class="outlined">
  <div class="p-4">
    <div class="flex align-center justify-between gap-3">
      <dl class="stats my-0">…</dl>
      <figure class="column-chart sparkline flex-1 my-0"
              style="--md-comp-column-chart-height: 72px">
        <figcaption class="visually-hidden">Monthly revenue, 2026</figcaption>
        <table><!-- … --></table>
      </figure>
    </div>
  </div>
</article>

Values and scale

Cells read the same way as in a line chart. A cell's number is its text without currency signs, percent signs or grouping commas. Text that is not a single number is a gap. For values like 4.4k or 4.400 €, put the number in data-value. In a grouped chart, a gap leaves an empty slot, so the other series stay in place.

The y axis always includes zero, with 10% of the span to spare above and, when there are negative values, below. Negative columns hang below zero and round their lower end. Set data-min or data-max on the figure to fix either end. When zero is off the scale, columns start at the bottom of the plot.

Add axis to label the y axis at round values, with a grid line at each. It widens an end the chart chose to the next round value, so the labels start and end on the plot's edges. The labels repeat any currency or percent sign the cells are written with and shorten large numbers, such as $1.5K. An end set with data-min or data-max stays where it is. The labels are hidden from assistive technology, and the table still carries the values.

Net cash flow, first half
MonthNet
Jan$1,200
Feb−$400
Mar$800
Apr$1,500
May−$900
Jun$600
<figure class="column-chart axis">
  <figcaption>Net cash flow, first half</figcaption>
  <table>
    <thead><tr><th>Month</th><th>Net</th></tr></thead>
    <tbody>
      <tr><th>Jan</th><td>$1,200</td></tr>
      <tr><th>Feb</th><td>−$400</td></tr>
      <!-- One row per month -->
    </tbody>
  </table>
</figure>

Accessibility

The table is the chart's text alternative. Give it a header row and a header cell per row, and name the chart with a <figcaption>. The drawn columns, labels and legend are hidden from assistive technology.

The plot is one Tab stop named by the caption. Left and right arrows move between groups, Home and End jump to the ends, and Escape hides the tooltip. In a right-to-left page the groups run from right to left, and the arrows still move the band the way they point. The tooltip is a polite live region, so a screen reader hears each group's values as it moves. A sparkline is not focusable. In forced colors, the columns keep their series colors so the legend still matches them.

JavaScript

AutoInit() starts every .column-chart. To start one yourself, call ColumnChart.init. After changing its table, call update() to redraw it in place, or put data-watch on the figure and it redraws itself. The columns move from their old values to the new ones, and a new column grows from zero. With reduced motion, they appear at once. The column chart takes the line chart's options and methods.

const chart = Expressive.ColumnChart.init(document.querySelector('.column-chart'), {
  max: 10000
});
chart.show(2);   // highlight the third group; -1 hides the tooltip
chart.update();  // redraw from the table; focus and the highlighted row stay
chart.destroy(); // remove the drawing and show the table again
OptionDefaultDescription
minnullLowest y value. data-min overrides it.
maxnullHighest y value. data-max overrides it.

Tokens

Override these on the .column-chart.

Token Default
--md-comp-column-chart-height240px, 48px for a sparkline
--md-comp-column-chart-column-width60% of each group's width
--md-comp-column-chart-shape4px
--md-comp-column-chart-track-color--md-sys-color-surface-container-highest
--md-comp-column-chart-grid-color--md-sys-color-outline-variant
--md-comp-column-chart-axis-width48px, the room beside the plot for axis labels
--md-comp-column-chart-color-1--md-sys-color-primary
--md-comp-column-chart-color-2--md-sys-color-tertiary
--md-comp-column-chart-color-3--md-sys-color-secondary
--md-comp-column-chart-color-4--md-sys-color-on-surface-variant

For a selective Sass build, include "column-chart" in $components.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow