Catalog · Charts
Column chart
Compares a few values as columns of one height scale, drawn from a native table that keeps every figure as text.
Fits best: app — a report that compares a few values with an order, such as days or months
Example
Parcels per day this week: the figure, its column, then the day.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
| 1,204 | 830 | 901 | 1,420 | 1,500 | 377 | 0 |
Several series
Visitors per month, by device, for three months: a row per month and a column per series, with no script. Each month is a group of columns, the head row the legend. The data is a sample: replace it with yours.
| Month | Desktop | Mobile |
|---|---|---|
| Jan | 186 | 80 |
| Feb | 305 | 200 |
| Mar | 237 | 120 |
Interactive
Visitors per day over four weeks, in <luz-chart type="column" interactive>. Each series and its total is a button that turns its columns off and on (the last one shown stays on); point at a day, or Tab to the chart and use the arrow keys, for its figures. Without JavaScript it is the table. The data is a sample: replace it with yours.
| Day | Desktop | Mobile |
|---|---|---|
| Apr 1 | 252 | 148 |
| Apr 2 | 271 | 276 |
| Apr 3 | 182 | 128 |
| Apr 4 | 380 | 247 |
| Apr 5 | 194 | 203 |
| Apr 6 | 191 | 74 |
| Apr 7 | 179 | 98 |
| Apr 8 | 179 | 132 |
| Apr 9 | 281 | 217 |
| Apr 10 | 187 | 171 |
| Apr 11 | 193 | 251 |
| Apr 12 | 278 | 125 |
| Apr 13 | 228 | 152 |
| Apr 14 | 120 | 100 |
| Apr 15 | 331 | 270 |
| Apr 16 | 319 | 125 |
| Apr 17 | 317 | 259 |
| Apr 18 | 271 | 122 |
| Apr 19 | 226 | 121 |
| Apr 20 | 187 | 86 |
| Apr 21 | 146 | 130 |
| Apr 22 | 206 | 248 |
| Apr 23 | 200 | 256 |
| Apr 24 | 248 | 253 |
| Apr 25 | 378 | 284 |
| Apr 26 | 216 | 136 |
| Apr 27 | 190 | 153 |
| Apr 28 | 199 | 94 |
Known issues
None known.
Code
Copy the markup
import "@luz-ds/catalog/components/column-chart.css";
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/column-chart.min.css" integrity="sha384-K41y3b0ofKFEbXhPjLBtb3Df9mEHL3G4TWMgg3TdG6Xv9wQTxynvP/vdgh/m44rk" crossorigin="anonymous">
A stylesheet of @luz-ds/catalog: link components/column-chart.css after luz.css.
<table class="luz-column-chart">
<caption>Parcels per day, this week</caption>
<thead>
<tr><th scope="col">Mon</th><th scope="col">Tue</th><th scope="col">Wed</th><th scope="col">Thu</th></tr>
</thead>
<tbody>
<tr><td style="--v: .8">1,204</td><td style="--v: .55">830</td><td style="--v: .6">901</td><td style="--v: .95">1,420</td></tr>
</tbody>
</table>
With several series, with no script: one row per category, its name in a <th scope="row">, and one cell per series, up to six. The head row names the series. The rows are laid out side by side, and some browsers (Safari with VoiceOver) then stop telling the table from a pile of boxes: write its roles, as here. <luz-chart interactive> writes them for you.
<table class="luz-column-chart" role="table">
<caption>Visitors per month, by device, January and February</caption>
<thead role="rowgroup">
<tr role="row"><th scope="col" role="columnheader">Month</th><th scope="col" role="columnheader">Desktop</th><th scope="col" role="columnheader">Mobile</th></tr>
</thead>
<tbody role="rowgroup">
<tr role="row"><th scope="row" role="rowheader">Jan</th><td role="cell" style="--v: .58">186</td><td role="cell" style="--v: .25">80</td></tr>
<tr role="row"><th scope="row" role="rowheader">Feb</th><td role="cell" style="--v: .95">305</td><td role="cell" style="--v: .62">200</td></tr>
</tbody>
</table>
Interactive, with the header of the series: link components/chart.css too, and load catalog.js, which loads chart.js when the page has a <luz-chart>. One row per day, its name in a <th scope="row">, and one column per series. The table has no class: the script gives it luz-column-chart when it draws it, so without the script it fits any width.
<luz-chart type="column" interactive>
<div class="luz-table-wrap" role="region" aria-label="Visitors per day, April 1 to 3" tabindex="0">
<table>
<caption>Visitors per day, April 1 to 3</caption>
<thead>
<tr><th scope="col">Day</th><th scope="col">Desktop</th><th scope="col">Mobile</th></tr>
</thead>
<tbody>
<tr><th scope="row">Apr 1</th><td style="--v: .5"><data value="252">252</data></td><td style="--v: .3"><data value="148">148</data></td></tr>
<tr><th scope="row">Apr 2</th><td style="--v: .54"><data value="271">271</data></td><td style="--v: .55"><data value="276">276</data></td></tr>
<tr><th scope="row">Apr 3</th><td style="--v: .36"><data value="182">182</data></td><td style="--v: .26"><data value="128">128</data></td></tr>
</tbody>
</table>
</div>
</luz-chart>
Variants and options
| Option | Effect |
|---|---|
--v | On each cell, from 0 to 1: its value over the top of the scale, the same for every column |
--luz-column-chart-color | Another chart colour for a table or a cell, such as var(--luz-chart-2) |
--luz-column-chart-h | The height of the columns |
--luz-column-chart-bar | The width of a column, var(--luz-space-lg) by default; never wider than its share of the table |
A row per category | Several series: a <th scope="row"> then a cell per series in each row, up to six. Each row is a group of columns, series n in --luz-chart-n, its name below it, and the head row is the legend |
--luz-column-chart-gap | In the CSS-only several-series chart, the space between the columns of a group, var(--luz-space-xs) by default; the groups take the space that is left. <luz-chart interactive> shares the width of its days and has no gap to set |
<luz-chart type="column" interactive> | Around a table of several series with no class: drawn as columns by the script, with a button per series and its total, which turns its columns off and on, and a readout of the day under the pointer or the focus |
How it works
A table of one head row of names and one row of figures. Each figure is the text of its cell, and the cell draws its column under it, from the foot up to --v, in --luz-chart-1, with the track in --luz-chart-track. The columns share the width evenly.
Work out --v as the value over the top of the scale, the same top for every cell. The inline style sets only that. With a strict style-src, set it in a rule of your own, such as .sat { --v: .25 }.
At the default text size, up to seven columns, or three groups of two, fit a 320 px screen. For more, or with larger text, put the table in a luz-table-wrap: it scrolls inside it.
The figure of each cell is above its column and the name below it, as an axis. With several series, each row is a group of columns, --luz-column-chart-gap apart (in the CSS-only chart), and the head row is the legend: each series' name after a dot of its colour.
Inside <luz-chart type="column" interactive>, the script adds a header above the table: a button per series, its name and the sum of its figures, written with the page's language. A sum suits counts and amounts, such as visitors or euros; it does not suit rates, shares or averages, whose total says nothing: do not use interactive for them, or leave their cells out of it. The caption becomes the title. The columns lose their track: the day under the pointer or the focus gets a light band, and the figures leave the columns for a readout beside it. The columns keep the scale of their --v when a series is turned off, and the last series shown cannot be turned off. The script writes no word of its own: the names, the figures and the caption are yours.
Without JavaScript
It needs none. Without the stylesheet it reads as a table: a head row of names, then the figures. The interactive chart is, without its script, a plain table: a row per day with every figure, which fits the narrowest screen. A table of a few categories that should be columns with no script takes the class luz-column-chart itself.
Keyboard and ARIA
The plain chart takes no focus. A table keeps its roles, so a screen reader reads each figure with its name. The caption names the table and says the unit. With several series the rows are laid out side by side: write the roles of the table in its markup, as the snippet does, so that Safari with VoiceOver still reads a table.
The interactive chart is a group named by the caption, and the script writes the roles of its table (table, rowgroup, row, columnheader, rowheader and cell). Its buttons are toggle buttons (aria-pressed): Enter or Space turns a series off and on, and the last series shown stays on. Its rows take the focus, one Tab stop, and each row is named by its own cells, from your text: “Apr 1, Desktop 252, Mobile 148”, with every series, shown or not. When the focus is in a row, the arrow keys move to the next or the previous day (Left and Right follow the direction of the page), and Home and End to the first and the last; the keys of a screen reader’s browse mode are its own.
The readout has the name of the day as its title, so the band is never the only sign of where you are; it is hidden from assistive technology. Escape puts it away without moving the pointer or the focus (it comes back on another day). A series turned off loses its columns but keeps its cells in the table, so a screen reader still reads every figure.
Do not hide the table: it is the data. The columns are a picture of it and carry no text of their own.
Contrast and focus are in the Design tab.
Design
When to use
Use it to compare a few values that have an order or a name, such as the days of a week or the months of a year, on one scale. With several series, it compares a few series category by category, such as two devices month by month. Use the interactive chart for two or more series over many days, when the totals matter and each day is read one at a time.
When not to use
Do not use the interactive chart when every figure must be seen at once: its figures are in the readout, nor for rates, shares or averages: the total of a series is a sum. Do not use it for one value (use a meter), for long names (use a bar list), or for a trend over many points. Keep a table when exact values matter most.
Do and don't
--v from one top of the scale, and write each figure in its cell: the height compares, the number says how much.Writing the content
Name the unit and the period in the caption (“Parcels per day, this week”), keep each name to a word or two, and write the figure with its unit when it is not obvious. Name each series in a word or two: its head cell is the name of its button. The data and the labels are yours: the component only draws them.
Contrast and focus
The column is a chart colour; the track is quieter on purpose, because the figure carries the value. The interactive chart has no track: the day under the pointer or the focus has a light band.
| Part | Colour on background | Light | Dark | High contrast | Needs |
|---|---|---|---|---|---|
Column against its track | --luz-chart-1 on --luz-chart-track | 3.4:1 | 3.8:1 | 6.8:1 | 2:1 |
Column on the page | --luz-chart-1 on --luz-surface-page | 5.6:1 | 7.7:1 | 14.1:1 | 3:1 |
Column on a card | --luz-chart-1 on --luz-surface | 5.9:1 | 7.2:1 | 14.1:1 | 3:1 |
Second series against its track | --luz-chart-2 on --luz-chart-track | 3.2:1 | 4.4:1 | 4.4:1 | 2:1 |
Second series on the page | --luz-chart-2 on --luz-surface-page | 5.3:1 | 8.9:1 | 9.3:1 | 3:1 |
Column on the band of the day under the pointer | --luz-chart-1 on --luz-surface-hover | 5.1:1 | 6.4:1 | 13.5:1 | 3:1 |
Second series on the band | --luz-chart-2 on --luz-surface-hover | 4.8:1 | 7.4:1 | 8.9:1 | 3:1 |
Figure | --luz-text-primary on --luz-surface-page | 15.8:1 | 15.1:1 | 21.0:1 | 4.5:1 |
Name of a column or a group | --luz-text-secondary on --luz-surface-page | 6.2:1 | 7.5:1 | 19.3:1 | 4.5:1 |
Name of a series in the legend | --luz-text-primary on --luz-surface-page | 15.8:1 | 15.1:1 | 21.0:1 | 4.5:1 |
Series shown: its name and total | --luz-text-primary on --luz-surface-muted | 15.8:1 | 15.1:1 | 20.1:1 | 4.5:1 |
Series shown: the line under it | --luz-brand-primary on --luz-surface-muted | 5.6:1 | 7.7:1 | 13.5:1 | 3:1 |
Readout: the figure | --luz-text-primary on --luz-surface-strong | 13.5:1 | 10.9:1 | 18.0:1 | 4.5:1 |
Readout: the series' name | --luz-text-secondary on --luz-surface-strong | 5.3:1 | 5.4:1 | 16.5:1 | 4.5:1 |
Focus ring | --luz-focus-ring on --luz-surface-page | 5.6:1 | 7.7:1 | 21.0:1 | 3:1 |
Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons; a quiet decorative part keeps 2:1 against what it is drawn on.
- Focus: the plain chart takes none. In the interactive chart, a button and a row take the focus ring,
--luz-focus-ring, 2px: inside a button's edge, and around the group of columns of a row. - Target and focus: each button of the interactive header is more than 24 by 24 px (WCAG 2.2, 2.5.8), and the readout is placed beside the day that has the focus and never over it (2.4.11). Both criteria are new in WCAG 2.2 and not yet required by EN 301 549 v3.2.1.
- Not colour alone: every column has its figure in text, and in forced colors the column is drawn in the system highlight with an edge around its track, a second series in the text colour; in the interactive chart, with no track, the day under the pointer or the focus has a dashed edge for its band. A series turned off has a hollow dot and a lighter name, not only another colour.
Tokens it uses
The 26 tokens that components/column-chart.css reads, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.
- Color
--luz-chart-1,--luz-chart-2,--luz-chart-3,--luz-chart-4,--luz-chart-5,--luz-chart-6,--luz-chart-track,--luz-divider,--luz-focus-ring,--luz-surface-hover,--luz-text-primary,--luz-text-secondary- Spacing and sizing
--luz-space-2xl,--luz-space-2xs,--luz-space-3xs,--luz-space-lg,--luz-space-md,--luz-space-sm,--luz-space-xs- Typography
--luz-font-mono,--luz-font-sans,--luz-text-sm,--luz-text-xs- Radius
--luz-radius-full,--luz-radius-sm- Layers
--luz-z-raised
Keyboard and ARIA are in the Code tab.