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.
| Month | Revenue |
|---|---|
| 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.
| Month | Basic | Premium | Enterprise |
|---|---|---|---|
| Jan | 5,500 | 3,500 | 3,200 |
| Feb | 6,400 | 3,200 | 3,700 |
| Mar | 7,600 | 3,400 | 2,700 |
| Apr | 5,900 | 4,000 | 3,100 |
| May | 5,800 | 4,300 | 4,500 |
| Jun | 7,600 | 4,300 | 4,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.
| Day | Fastest to slowest | Median |
|---|---|---|
| Mon | 120–480 | 210 |
| Tue | 95–390 | 180 |
| Wed | 140–620 | 260 |
| Thu | 110–450 | 200 |
| Fri | 160–710 | 310 |
<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.
| Month | Basic | Premium | Enterprise |
|---|---|---|---|
| Jan | 5,500 | 3,500 | 3,200 |
| Feb | 6,400 | 3,200 | 3,700 |
| Mar | 7,600 | 3,400 | 2,700 |
| Apr | 5,900 | 4,000 | 3,100 |
| May | 5,800 | 4,300 | 4,500 |
| Jun | 7,600 | 4,300 | 4,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.
| Month | Revenue |
|---|---|
| Jan | $4,400 |
| Feb | $3,400 |
| Mar | $3,700 |
| Apr | $4,900 |
| May | $4,600 |
| Jun | $5,000 |
| Month | Revenue |
|---|---|
| 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 Day Signups Mon 42 Tue 38 Wed 51 Thu 47 Fri 56 Sat 61 Sun 58
<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:
- Update the stat.
- Swap the table rows.
-
Call
update()to redraw. If the figure hasdata-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
| Month | Expenses | Profit |
|---|---|---|
| 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
| Day | Signups |
|---|---|
| Mon | 42 |
| Tue | 38 |
| Wed | 51 |
| Thu | 47 |
| Fri | 56 |
| Sat | 61 |
| Sun | 58 |
- Revenue
- $54,500
- +6.2% from last year
| Month | Revenue |
|---|---|
| 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.
| Month | Net |
|---|---|
| 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
| Option | Default | Description |
|---|---|---|
min | null | Lowest y value. data-min overrides it. |
max | null | Highest y value. data-max overrides it. |
Tokens
Override these on the .column-chart.
| Token | Default |
|---|---|
--md-comp-column-chart-height | 240px, 48px for a sparkline |
--md-comp-column-chart-column-width | 60% of each group's width |
--md-comp-column-chart-shape | 4px |
--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-width | 48px, 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.