# Charts

_Box Kite 2.2.6 · a markdown copy of https://www.box-kite.dev/charts/_

Four micro-primitives — sparkline, progress ring, gauge and mini donut — built from the SVG components, styled with the props everything else here takes, and cheap enough to put one in every row of a ten-thousand-row grid.

## Not a chart library

These are the small, dense drawings a dashboard is made of, and nothing more: no axes, no legends, no data transformations. What they give you instead is that a chart is `a Box` — its colour, its size, its dark mode, its hover state and its breakpoints are the props you already know, and there is no second styling system to learn or to theme. For a real chart with axes and a tooltip, reach for Recharts and wrap it in a `ChartContainer`, which is further down this page; these are for the twenty places a dashboard needs a shape rather than a chart.

**Sparkline**

```jsx
<Flex gap={8} ai="center" flexWrap="wrap">
  <Sparkline data={[4,9,6,12,10,15,13,18]} width="7rem" color="sky-500" />
  <Sparkline
    data={[4,9,6,12,10,15,13,18]}
    width="7rem"
    variant="area"
    color="violet-500"
  />
  <Sparkline
    data={[4,9,6,12,10,15,13,18]}
    width="7rem"
    variant="bar"
    color="emerald-500"
  />
</Flex>
```

## A sparkline fills its box

A sparkline is the one primitive that is not drawn to scale: it is `100%` wide and stretches to whatever you give it, which is what makes it usable in a table cell of unknown width. The line stays one width thick anyway, because `vectorEffect="non-scaling-stroke"` is on by default — the property SVG has for exactly this, and an ordinary prop, so `vectorEffect="none"` turns it off. Everything else is inherited from the `<svg>`: `color` paints it (the default stroke is `currentColor`), `strokeWidth` thickens it, and both take a `hover`, a `theme` and a breakpoint.

**One scale for many rows**

```jsx
// Each sparkline scales to its own data by default — good alone, misleading in a column.
<Sparkline data={[4, 9, 6, 12]} />

// min and max fix the axis, so two rows can be compared at a glance.
<Sparkline data={[4, 9, 6, 12]} min={0} max={20} />
<Sparkline data={[2, 3, 2, 4]} min={0} max={20} />
```

**A gradient is a value now**

```jsx
<Sparkline
  data={[4,9,6,12,10,15,13,18]}
  variant="area"
  width="16rem"
  height="4rem"
  stroke="url(#trend)"
  fill="url(#trend)"
>
  <Defs>
    <LinearGradient
      id="trend"
      x1="0"
      y1="0"
      x2="1"
      y2="0"
    >
      <Stop offset="0%" stopColor="currentColor" color="sky-500" />
      <Stop offset="100%" stopColor="currentColor" color="violet-500" />
    </LinearGradient>
  </Defs>
</Sparkline>
```

## …which it was not before

`fill` and `stroke` used to take a colour token and nothing else, so a gradient had to be written as an attribute — `props={{ fill: 'url(#sky)' }}` — which put the paint outside the theme system and outside every pseudo-class. Both props now also accept a reference to something the document defines (`url(#trend)`) or a variable somebody else declared (`var(--chart-1)`), so a gradient fill can change on `hover` and per theme like any other value. `clipPath` takes a `url(#…)` the same way. A typo still produces nothing at all rather than a broken declaration: the definition names the two shapes it accepts.

**Progress ring**

```jsx
<Flex gap={8} ai="center" flexWrap="wrap">
  <ProgressRing value={0.25} color="sky-500" />
  <ProgressRing value={0.62} color="emerald-500" thickness={16} />
  <ProgressRing value={0.9} color="amber-500" thickness={6}>
    <SvgText
      x={50}
      y={57}
      textAnchor="middle"
      fontSize={26}
      fill="amber-500"
      stroke="none"
    >
      90
    </SvgText>
  </ProgressRing>
</Flex>
```

**The arc eases with no animation code**

```jsx
const [value, setValue] = useState(0.35);

<Flex gap={6} ai="center">
  <ProgressRing value={value} color="violet-500" width="5rem" height="5rem" label="Progress" />
  <Button onClick={() => setValue(0.75)}>75%</Button>
</Flex>
```

## Why that moves

The filled part of a ring is a dash on its outline, and a dash length is an ordinary style prop — so it lands in a CSS class, and every shape inside an `<svg>` already transitions. Setting a number is all the JavaScript there is; the easing is the stylesheet, and it stops on its own for a visitor who asked for `prefers-reduced-motion`.

**Gauge**

```jsx
<Flex gap={8} ai="center" flexWrap="wrap">
  <Gauge value={0.4} color="sky-500" />
  <Gauge
    value={0.75}
    color="rose-500"
    sweep={180}
    start={270}
  >
    <SvgText
      x={50}
      y={48}
      textAnchor="middle"
      fontSize={22}
      fill="rose-500"
      stroke="none"
    >
      75%
    </SvgText>
  </Gauge>
  <Gauge
    value={0.55}
    color="emerald-500"
    sweep={360}
    thickness={6}
  />
</Flex>
```

## A dial is an arc you choose

`sweep` is how far round it goes and `start` is where it begins, in degrees clockwise from twelve o'clock — three quarters of a turn from the bottom left by default, a half turn for a speedometer, a whole turn for a ring. Both are constants of the shape rather than of the data, so every gauge of the same shape shares one path string.

**Mini donut**

```jsx
<Flex gap={8} ai="center" flexWrap="wrap">
  <MiniDonut data={[5,3,2]} />
  <MiniDonut
    data={[8,5,3,2,1]}
    thickness={12}
    width="4rem"
    height="4rem"
  />
  <MiniDonut
    data={[6,4]}
    colors={["violet-500","violet-200"]}
    thickness={30}
  />
</Flex>
```

## Colours, and where they come from

A donut is the one primitive that needs more than one colour, so it takes a list and cycles it. Each entry is anything the `fill` prop accepts — a token like `sky-500`, or `var(--chart-1)` if your design system publishes a chart palette. The values need no total: each is drawn as its share of the whole.

**Ten thousand rows, one sparkline each**

```jsx
// A cell renderer is a component: define it outside the render, or every scroll remounts the column.
function TrendCell({ cell }: { cell: { row: { data: Row } } }) {
  return (
    <Flex px={3} ai="center" height="fit">
      <Sparkline data={cell.row.data.trend} color="emerald-500" />
    </Flex>
  );
}

<DataGrid
  data={rows}
  def={{
    rowKey: 'id',
    rowHeight: 36,
    columns: [
      { key: 'name', header: 'Account' },
      { key: 'revenue', header: 'Revenue', align: 'right' },
      { key: 'trend', header: 'Last 12 months', width: 180, Cell: TrendCell },
    ],
  }}
/>
```

## Why ten thousand rows is affordable

Two different answers, and the split is the whole trick. A sparkline's _shape_ is the `d` attribute, which the styling engine never sees — ten thousand different shapes generate no CSS at all, and what _is_ a style prop (the colour, the width) is shared, so the rows share one rule each. A ring's fill, on the other hand, has to be a style prop to be able to transition, so it lands in a class name — and the fraction is rounded to half a percent, which caps a column of percentages at a couple of hundred rules instead of one per row. Half a percent of a 48px ring is a third of a pixel.

## A CSS variable is a Box prop

Every prop on this page becomes a CSS declaration inside a class. `vars` is the one whose _names_ come out of the value: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` on the element, and everything inside it inherits it — including markup this library never rendered. Because it is an ordinary prop it nests inside `theme`, `hover` and a breakpoint like all the others, and it lands in a class, so two subtrees declaring the same palette share one rule rather than each carrying a `<style>` tag of its own.

**A Recharts chart, themed by the page it sits on**

```jsx
import { ChartContainer } from '@box-kite/react/components/chart';
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from 'recharts';

<ChartContainer
  series={['revenue', 'cost']}
  vars={{ 'chart-grid': 'slate-200', 'chart-label': 'slate-500' }}
  theme={{ dark: { vars: { 'chart-grid': 'slate-800', 'chart-label': 'slate-400' } } }}
  height={60}
>
  <ResponsiveContainer width="100%" height="100%">
    <AreaChart data={months}>
      <CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" vertical={false} />
      <XAxis dataKey="month" stroke="var(--chart-label)" fontSize={12} />
      <YAxis stroke="var(--chart-label)" fontSize={12} width={36} />
      <Area dataKey="revenue" stroke="var(--color-revenue)" fill="var(--color-revenue)" fillOpacity={0.15} strokeWidth={2} />
      <Area dataKey="cost" stroke="var(--color-cost)" fill="var(--color-cost)" fillOpacity={0.15} strokeWidth={2} />
    </AreaChart>
  </ResponsiveContainer>
</ChartContainer>
```

## The chart names no colour

Flip the theme in the header and every line above changes with it — the chart code does not. That is the whole of

`ChartContainer`

: it declares

`--chart-1`

…

`--chart-6`

in both themes and one

`--color-<series>`

per name you give it, so

`stroke="var(--color-revenue)"`

is all the chart ever says about paint. The two names it does

_not_

know about — the grid and the axis labels — are the same mechanism spelled out by hand, which is what

`vars`

is for.

The names are deliberately the ones the ecosystem already uses, so a chart lifted out of shadcn's charts works unchanged. What is different is where they live: there is no `<style>` tag per chart and no `id` to scope it with, because a Box prop is already scoped to its element — and two tiles with the same series share the rule.

## Naming a picture of numbers

Every primitive follows `Svg`'s rule, and a chart is the case where it matters most: with no `label` the drawing is `aria-hidden`, and with one it is `role="img"` with that name. Both are right in different places. A sparkline beside the number it summarises is decoration — leave it unnamed, or a screen reader reads the row twice. A sparkline that is the only thing in a cell is the data, so give it a label that says what a sighted reader gets from the shape: `label="Revenue, rising 12% over six months"`, not `label="Chart"`.

## Sparkline props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `data`required | `readonly number[]` | — | The numbers to draw, oldest first. |
| `variant` | `'line' \| 'area' \| 'bar'` | `'line'` | `line` (default), `area` — the line with the space beneath it filled — or `bar`. |
| `min` | `number` | — | Fix the ends of the value axis. Without them the line uses its own lowest and highest value, so every row of a table is scaled to itself; with them a column of sparklines is comparable. |
| `max` | `number` | — | The top of the value axis — see `min`, which it is written beside. |
| `preserveAspectRatio` | `string` | — | How the drawing is fitted when the box it is given is a different shape. |
| `width` | `string \| number` | — | The `width` attribute: a number in user units, or any CSS length — `"100%"`, `"12rem"`. |
| `height` | `string \| number` | — | The `height` attribute: a number in user units, or any CSS length. |
| `label` | `string` | — | Names the drawing — `role="img"` and this text. Without one the `<svg>` is `aria-hidden`. |
| `children` | `React.ReactNode` | — | What is drawn on top of the shape: the number inside a ring, a gradient in a `<Defs>`. SVG content, not a render prop. |

## Sparkline accessibility

- The `<svg>` is `aria-hidden` unless it is given a `label`, because a sparkline usually repeats a number that is already in the row beside it.
- A chart is not a substitute for the values: where the shape *is* the information, give it a `label` saying what it shows, and keep the numbers reachable in text.

Swept with axe on every release, in 2 states: `Sparkline`, `Sparkline (decoration)`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## Sparkline style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`sparkline`

`sparkline.line`

`sparkline.area`

`sparkline.bar`

## ProgressRing props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `value`required | `number` | — | How full the arc is, from 0 to 1. Rounded to half a percent — see `ChartUtils.FRACTION_STEP`. |
| `thickness` | `number` | `10` | The width of the ring in view units, out of the 100 the box is wide. |
| `trackOpacity` | `NonNullable<BoxStyleProps['strokeOpacity']>` | — | The unfilled part of the ring, as the same colour faded — so the track follows the theme too. Default `0.2`, from the style node. |
| `preserveAspectRatio` | `string` | — | How the drawing is fitted when the box it is given is a different shape. |
| `width` | `string \| number` | — | The `width` attribute: a number in user units, or any CSS length — `"100%"`, `"12rem"`. |
| `height` | `string \| number` | — | The `height` attribute: a number in user units, or any CSS length. |
| `label` | `string` | — | Names the drawing — `role="img"` and this text. Without one the `<svg>` is `aria-hidden`. |
| `children` | `React.ReactNode` | — | What is drawn on top of the shape: the number inside a ring, a gradient in a `<Defs>`. SVG content, not a render prop. |

## ProgressRing accessibility

- `aria-hidden` with no `label`, and a `label` makes it `role="img"`. For progress a user is *waiting* on, put the value in text or in a `role="progressbar"` of your own — an image role does not announce a change.

Swept with axe on every release, in this state: `ProgressRing`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## ProgressRing style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`progressRing`

`progressRing.track`

`progressRing.arc`

## Gauge props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `sweep` | `number` | `270` | How far round the dial goes, in degrees. Three quarters of a turn by default. |
| `start` | `number` | `225` | Where it starts, in degrees clockwise from twelve o'clock. Bottom left by default. |
| `value`required | `number` | — | How full the arc is, from 0 to 1. Rounded to half a percent — see `ChartUtils.FRACTION_STEP`. |
| `thickness` | `number` | `10` | The width of the ring in view units, out of the 100 the box is wide. |
| `trackOpacity` | `NonNullable<BoxStyleProps['strokeOpacity']>` | — | The unfilled part of the ring, as the same colour faded — so the track follows the theme too. Default `0.2`, from the style node. |
| `preserveAspectRatio` | `string` | — | How the drawing is fitted when the box it is given is a different shape. |
| `width` | `string \| number` | — | The `width` attribute: a number in user units, or any CSS length — `"100%"`, `"12rem"`. |
| `height` | `string \| number` | — | The `height` attribute: a number in user units, or any CSS length. |
| `label` | `string` | — | Names the drawing — `role="img"` and this text. Without one the `<svg>` is `aria-hidden`. |
| `children` | `React.ReactNode` | — | What is drawn on top of the shape: the number inside a ring, a gradient in a `<Defs>`. SVG content, not a render prop. |

## Gauge accessibility

- `aria-hidden` with no `label`, and a `label` makes it `role="img"`. The value itself belongs in text: a dial's angle is not readable by anyone who cannot see it.

Swept with axe on every release, in this state: `Gauge`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## Gauge style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`gauge`extends `progressRing`

`gauge.track`

`gauge.arc`

## MiniDonut props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `data`required | `readonly number[]` | — | One value per segment. Each is drawn as its share of the whole, so they need no total. |
| `colors` | `readonly Paint[]` | `DEFAULT_COLORS` | One colour per segment, cycled if there are fewer than there are values. |
| `thickness` | `number` | `20` | The width of the ring in view units, out of the 100 the box is wide. |
| `preserveAspectRatio` | `string` | — | How the drawing is fitted when the box it is given is a different shape. |
| `width` | `string \| number` | — | The `width` attribute: a number in user units, or any CSS length — `"100%"`, `"12rem"`. |
| `height` | `string \| number` | — | The `height` attribute: a number in user units, or any CSS length. |
| `label` | `string` | — | Names the drawing — `role="img"` and this text. Without one the `<svg>` is `aria-hidden`. |
| `children` | `React.ReactNode` | — | What is drawn on top of the shape: the number inside a ring, a gradient in a `<Defs>`. SVG content, not a render prop. |

## MiniDonut accessibility

- `aria-hidden` with no `label`, and a `label` makes it `role="img"`. Segments are told apart by colour alone, so the breakdown has to exist somewhere a reader can get at it.

Swept with axe on every release, in this state: `MiniDonut`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## MiniDonut style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`miniDonut`

`miniDonut.segment`

## ChartContainer props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `series` | `ChartSeries` | — | The series drawn inside, and what colour each of them is. |

## ChartContainer accessibility

- It is a `<div>` with variables on it — no role, no name, nothing in the accessibility tree. Whatever the chart library renders inside owns its own accessibility.

Swept with axe on every release, in this state: `ChartContainer`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## ChartContainer style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`chartContainer`

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

