---
title: Bar Chart
description: One measure across a handful of categories, drawn by TanStack Charts and coloured from the site's own tones.
source: https://adamjurek.com/components/bar-chart
---

## Home


One paragraph on what this does and when to reach for it. This is the first
thing on the component's page and the line that goes into `llms.txt`, so it
should survive being read on its own.

<!-- ::start:showcase demo="bar-chart" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

The decision somebody would otherwise have to reverse-engineer from the source.
Not what the code does - the source says that - but what it is avoiding.

## What it does not do

The boundary. A component that lists what it is not is a component you can
decide against in ten seconds.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack, shadcn and pnpm
> commands appended to the bottom of this tab, along with its version,
> dependencies and files. Do not add your own - the generated ones cannot go
> stale, and a second copy immediately does.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/bar-chart.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/bar-chart.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Data |
| Files | `bar-chart.tsx` |
| Dependencies | `@tanstack/charts@^0.14.0`, `@tanstack/react-charts@^0.14.0`, `d3-scale@^4.0.2` |
| Tags | data, chart, tanstack, theme-aware |


## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { BarChart } from "@sushindustries/ui";
import "@sushindustries/atoms/atoms.css";

export function Example() {
	return (
		<BarChart
			label="Tokens per document kind"
			description="Source files are two thirds of the index by weight."
			rows={[
				{ label: "source", value: 412_310 },
				{ label: "component", value: 188_004 },
				{ label: "note", value: 74_920 },
			]}
		/>
	);
}
```

## What you should see

Three horizontal bars in the accent colour, longest at the top, with the
category names read straight across on the left and a rounded tick scale
underneath. `label` and `description` are announced and never drawn. Switch the
site's theme and the bars change with it, because the fill is a custom property
rather than a colour passed in.

## If nothing happens

Bars in the browser's default black with unstyled axis text mean
`@sushindustries/atoms/atoms.css` was never imported - `--chart-fill`,
`--chart-line` and `--chart-text` are declared on `.chart` in that stylesheet,
and an undefined custom property leaves the library drawing with its own
default. An empty `rows` is not an error: it draws `Nothing to draw yet.`
instead.


## Guides


## Composing it

Two accessors, a direction and a height. Anything that needs a second series, a
stack or a time axis has outgrown this and should call `defineChart` directly,
which is why `@tanstack/charts` is a dependency rather than something hidden
behind a wrapper.

```tsx
<BarChart
	label="Tokens per document kind"
	description="Source files are two thirds of the index by weight."
	rows={counts.map((one) => ({ label: one.kind, value: one.tokens }))}
	height={180}
/>
```

`description` is the finding, not the data: "source files are two thirds of the
index" rather than "a bar chart with nine bars". The numbers belong in the
table beside it.

## Colour comes from the stylesheet

`--chart-fill` is read off the rendered element at paint time, so a chart flips
with the theme like everything else and no colour is written down twice. That
is the one thing a charting library will always get wrong, because it cannot
know about `data-theme` - and a chart is where a hard-coded colour is most
obvious, since a whole bar goes the wrong way rather than a one-pixel border.

```css
.chart {
	--chart-fill: var(--accent);
	--chart-line: var(--line);
	--chart-text: var(--fg-faint);
}
```

## The three variants

`colorByCategory` cycles six of the site's own category tones, so a chart is
visibly the same palette as the nav and the badges. They are assigned by the
label's position in the data rather than by name, so re-sorting the rows keeps
`component` the colour it was.

| Prop | Value | For |
| --- | --- | --- |
| `direction` | `bar` (default) | Runs left to right, so word-shaped category labels read straight across instead of being rotated or truncated |
| `direction` | `column` | Runs bottom to top. For a few short labels, or a sequence people read as time |
| `colorByCategory` | `true` | One tone per category. For comparing kinds - never for a ranking |

## Why colour is off by default

On a single series colour carries no information: the axis already says which
bar is which, so colouring them differently is decoration that looks like
meaning. Turn it on when the categories are the subject rather than the scale.
That is "tokens per kind" and it is not "the ten most viewed pages", where the
reader is following the length and the colours only argue with it.

## When not to use it

One measure across a handful of categories is the whole remit. There is no
`format` prop - there was one for an hour, then the axes moved to the library's
own scales, which draw their own ticks, and it became a prop that took a
function and ignored it. If ticks ever need reformatting it belongs beside the
scale, not as a parallel option here.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `rows` | `readonly BarChartDatum[]` | - |  |
| `label` | `string` | - | Announced to screen readers, and never drawn. Required, like a caption. |
| `description?` | `string` | - | One sentence for anyone who cannot see it, saying what the shape shows. A chart's alt text is the finding, not the data - "source files are two thirds of the index" rather than "a bar chart with nine bars". The table beside it is where the numbers are. |
| `direction?` | `"bar" \| "column"` | `"bar"` | `bar` runs left to right, `column` bottom to top. |
| `colorByCategory?` | `boolean` | `false` | One colour per category, cycled from the site's tones. Off by default, and that default is the honest one: colour on a single series carries no information - the axis already says which bar is which, so colouring them differently is decoration that looks like meaning. Turn it on when the categories are the subject rather than the scale, so a reader is comparing *kinds* rather than reading a ranking. That is the case for "tokens per kind" and not for "the ten most viewed pages". |
| `height?` | `number` | `220` |  |

<!-- /generated:api -->

## Notes

Anything the types cannot say: which combinations are meaningless, which
prop is ignored when another is set, and what it does when handed
something it cannot render.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="bar-chart" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## The shape above the numbers

A chart and the table it summarises, which is the pairing that makes the chart
worth drawing: the shape answers "which kind is the index mostly made of", and
anyone who needs the figure reads it off the table underneath.

```tsx
import { BarChart, DataTable } from "@sushindustries/ui";

export function IndexWeight({ counts }) {
	return (
		<section className="flex col gap-4">
			<BarChart
				label="Tokens per document kind"
				description="Source files are two thirds of the index by weight."
				rows={counts.map((one) => ({ label: one.kind, value: one.tokens }))}
				colorByCategory
				height={180}
			/>
			<DataTable
				label="Tokens per document kind"
				rows={counts}
				sortBy="tokens"
				descending
				density="compact"
				columns={[
					{ id: "kind", header: "Kind", sortable: true },
					{
						id: "tokens",
						header: "Tokens",
						numeric: true,
						sortable: true,
						cell: (row) => row.tokens.toLocaleString(),
					},
				]}
			/>
		</section>
	);
}
```

## A short sequence, read as time

Four quarters is the case for `column`: the labels are short enough to sit
under a bar, and people read a sequence left to right.

```tsx
<BarChart
	label="Posts published per quarter"
	direction="column"
	rows={quarters}
	height={160}
/>
```

## What this example is not

`colorByCategory` is right in the first example because the kinds are the
subject, and it would be wrong on the second - four quarters are a sequence,
and colouring them would imply a difference the reader would go looking for.
Neither chart formats its own ticks. And `rows` is already summed here; nothing
in this component groups or aggregates anything.
