Skip to content
Folio

Charts

BarChart

Discrete categories compared by length.

Usage

When to reach for it

The categories are buckets rather than a continuum. If the axis is time and the reader is following a trend, an AreaChart or LineChart reads it faster.

Ships from@misoto22/design/charts

TSX
import { BarChart } from '@misoto22/design/charts'

Notes

Discrete categories compared by length — the shape for "how much, per thing".

Reach for <AreaChart> or <LineChart> when the axis is continuous and the reader is following a trend rather than comparing buckets.

Anatomy

BarChart anatomy
ElementDescription
Figure framerequiredChartFigure’s <figure>, named by title whether or not the page prints a heading above it.
Barsrequired<BarChart.Bar>, drawn through a custom shape: a transparent rectangle for the hit area, then the painted bar three pixels shorter than its slot so a stacked segment keeps a hairline of page between it and the one above. A bar shorter than that trim is floored at one pixel rather than taken to nothing, so a small count is never pixel-identical to an absent one.
Axes<BarChart.XAxis> and <BarChart.YAxis>, both flat by default — no tick line, no axis line. Every Recharts prop passes straight through, domain included, which is the door a truncated baseline comes in through.
Legend<BarChart.Legend>. With isClickable each entry is a real <button> carrying aria-pressed rather than a div with a handler, which is the difference between a filter a keyboard can reach and one it cannot.
Value labels<BarChart.Values>, a slot composed inside a bar. show defaults to last; all is for five or six bars where the exact figures are the point, and past that it is a table wearing a chart.
Hidden data tableThe sr-only table of the FULL data rather than of the brushed window, so a reader on the table is never shown less than the CSV export holds. hideDataTable removes it, and zero rows render nothing.
Empty stateChartEmpty, rendered in place of the plot when data is empty — a title, a reason and an optional action, so a filter that matched nothing is told apart from a load that failed. empty={false} keeps the bare axes instead.

Best practices

Do

  • Reach for buffer on a period still open. It hatches the last ROW rather than the last bar on screen, so brushing back into the middle of the range hatches nothing — a month that closed in March is never drawn as still being counted.
  • Leave the value axis anchored at zero. A bar encodes by LENGTH from the baseline, so a domain of ["dataMin", "dataMax"] passed through <BarChart.YAxis> turns a two percent gap into a doubled bar. This is the distortion a bar chart cannot survive and a LineChart can: a line encodes by slope, so clipping its domain rescales the reading rather than inventing one.
  • Reach for orientation="horizontal" when the category names are long. The alternative is a tick label rotated under every column, and a rotated label is slower to read than the bar it names.
  • Pass a tickFormatter to <BarChart.YAxis> under stackType="percent". Unlike AreaChart’s expanded stack, which swaps in percentTick itself, the bar chart’s axis keeps defaultTick — so a normalised chart reads 0 to 1 instead of 0% to 100%.
  • Put the series that has to be compared across categories at the BASELINE of a stack. Only the bottom segment starts at zero; every band above it floats on the ones below, and reading a third band across twelve months is a comparison the eye cannot make. When that comparison is the point, group the bars instead.

Don’t

  • hideDataTable leaves no exact figure anywhere. defaultTick compacts at ten thousand and above, so the axis says 1.2M and so do the <BarChart.Values> labels; the sr-only table, where every cell is a full toLocaleString, was the only place the real number was written.
  • Twenty bars at variant="default" is a wall rather than twenty values. stripped draws a 2px cap over a wash and stays countable at that density, which is the density it exists for.
  • Do not close barCategoryGap up. The space between groups is the only thing telling a reader that two adjacent bars are two series rather than two categories, so a grouped chart with no category gap reads as a stacked one.

Examples

default

Two bar series over one category axis, which is the shape to reach for when the categories are discrete rather than a continuum. Each series takes the next slot on the grey ramp in declaration order, so hiding one never repaints the survivors; the second carries a hatch, because a step of grey alone is a thin thing to ask a reader to hold in their head. That is also why the legend is not optional above one series.

Visitors by month
Visitors by month
monthDesktopMobile
Jan18680
Feb305200
Mar237120
Apr273190
May209130
Jun314240

fill variants

Six ways to fill a bar, cycled on one series so they are comparable. stripped is the one that survives density best: it rounds only the top corners and prints a 2px cap of the series colour above a wash, so at twenty bars it still reads as twenty distinct values where a solid block has become a wall. The rest trade weight for texture, and with no hue to spend this is what separates two series before the ramp does.

Visitors by month — default
Visitors by month — default
Desktop
186
305
237
273
209
314

stacking and orientation

Two choices that interact. Stacking asks a different question at each setting — default compares, stacked totals, percent shares — while orientation is about the category names: "Organic search" reads straight beside a row and has to be rotated or truncated under a column. The axes swap with it, which is the one part the call site does by hand: the category dataKey moves from the X axis to the Y, and that axis needs a width wide enough for the longest label.

Visitors by channel — default, vertical
Visitors by channel — default, vertical
DesktopMobile
18680
305200
237120
173190

emphasis

Three ways to say "this one", side by side. A monochrome chart has no brighter colour to reach for, so glowing puts a halo behind the series that is the point of the figure; buffer draws the last category as an open hatch, the idiom for a period still in progress, the same height as any other bar but visibly not the same kind of fact; and hover highlight drops every bar the pointer is not on to 30%. That last one is driven by the pointer, so it can never be the only thing carrying a reading.

Glowing
Glowing
Desktop
186
305
237
273
209
314
Buffer — the last period is still open
Buffer — the last period is still open
Desktop
186
305
237
273
209
314
Hover highlight
Hover highlight
Desktop
186
305
237
273
209
314

interaction

What a reader can do with a chart rather than what it looks like. The legend entries are real buttons with aria-pressed, so Tab reaches them and Enter toggles a series — a styled div with a click handler could do neither — and the bars take a click to select for the same reason. The tooltip's ground and its corner are the two knobs worth exposing, and defaultIndex opens it on a row before anything is hovered, so the panel is legible in a screenshot and to a reader who never moves a pointer. Both knobs move the floating panel and nothing else on the card — and frosted is the plot showing THROUGH that panel, so it reads where the panel crosses a bar and nowhere else: over the white ground it is white at 75% over white, which is white.

Visitors by month
Visitors by month
DesktopMobile
18680
305200
237120
273190

brush

Twenty-eight bars is where a category axis stops being readable, and the brush keeps the whole series in view while the reader chooses a slice of it. The strip below draws the same rows with the bar mark, so what is being chosen from looks like what is being read — a control that shows only the zoomed result cannot do that. The chart above then renders just the selected window, so everything reading the visible rows follows the selection with it.

Visitors by day
Visitors by day
dayDesktop
D190
D2176
D3241
D4270
D5254
D6198
D7115
D8153
D9226
D10266
D11263
D12217
D13140
D14129
D15208
D16259
D17268
D18234
D19164
D20104
D21188
D22248
D23270
D24248
D25187
D26102
D27166
D28235

loading

The skeleton, with its bar count set by hand. loadingBars decides how many bars are drawn and otherwise defaults to twelve, which matters more on a bar chart than on a line: a skeleton of twelve followed by six real bars reads as the chart having lost half its data. The marks are swapped but the measured height is kept, and a brush or a toolbar composed alongside is suppressed while this is on, because there is nothing yet to choose a window from.

Visitors by month
Loading

value labels

Printing the numbers on the marks, and how few of them to print. A number on every point is the most common way a chart is spoiled — the labels compete with the shape they annotate and the reader loses both — so the default is last, the one value a reader would otherwise trace back to the axis for. extremes is what "which month was worst" actually asks; all earns its place only where five or six bars and their exact figures are the whole point, past which it is a table wearing a chart.

Visitors by month
Visitors by month
Desktop
1,860
30,500
23,700
27,300
9,200
31,400

sonify

Two series on screen and one worth listening to. keys narrows the run, because playback is sequential — each series plays in full, one after another — and a listener rarely wants all of them. formatValue is what the announcement speaks, so the range is read out in the units the axis prints, "from $9,200 to $31,400" rather than "9200"; leave it off and the spoken range falls back to the default tick format, which is not the one on the axis beside it.

Revenue by month
Revenue by month
monthRevenueRefunds
Jan18,6001,240
Feb30,500980
Mar23,7002,310
Apr27,3001,150
May9,2003,040
Jun31,400870

toolbar

The toolbar tops out at five controls and ships no overflow menu, which is a size decision as much as a design one: a row that can never outgrow five 44px targets does not need one, and the menu would cost every consumer of every cartesian chart the Radix menu the chart reaches statically. A chart in a narrow card drops controls instead — zoom={false}, or one export format rather than two. The two files are deliberately not the same picture: the PNG is the plot as it stands and follows the zoom, while the CSV is always the whole dataset, like the figure's hidden data table, because a spreadsheet quietly missing the rows you had zoomed past is data loss you cannot see.

Visitors by month

Arrow keys pan by one point, Page Up and Page Down by a screenful, Home and End jump to the ends. Plus and minus zoom, zero resets. Ctrl and the wheel zoom around the pointer, and dragging across the plot zooms to that span.

Showing Jan 25 to Dec 26, 24 of 24 points

Visitors by month
monthDesktop
Jan 251,200
Feb 251,534
Mar 251,837
Apr 252,077
May 252,235
Jun 252,296
Jul 252,258
Aug 252,131
Sep 251,932
Oct 251,687
Nov 251,428
Dec 251,189
Jan 26999
Feb 26884
Mar 26861
Apr 26937
May 261,108
Jun 261,360
Jul 261,669
Aug 262,005
Sep 262,337
Oct 262,631
Nov 262,861
Dec 263,004

Types

TSX
export type BarVariant = 'default' | 'hatched' | 'duotone' | 'duotone-reverse' | 'gradient' | 'stripped'
export type BarStackType = 'default' | 'stacked' | 'percent'
export type BarOrientation = 'vertical' | 'horizontal'

Accessibility

  • title is required; the rows are also rendered as a visually hidden table.
  • Every bar carries an invisible full-height hit rectangle, so a 3px bar at the bottom of the scale is as easy to hit as a full-height one.
  • A clickable legend entry is a real button with aria-pressed, not a div with a click handler.
  • The staggered grow-in is anchored to the chart’s own start rather than to each bar’s mount, so a hover cannot replay it — and reduce-motion drops it entirely.