Chart

The core every chart family is built on — the host, the palette, the shared frame, and the house defaults.

const data = /* ... */;

<BarChart
  data={data}
  x="month"
  y={['desktop', 'mobile']}
  labels={{ desktop: 'Desktop', mobile: 'Mobile' }}
  formatY={{ locale: 'en-US', number: { notation: 'compact' } }}
  ariaLabel="Desktop and mobile visitors per month, side by side"
/>

Every chart family — area, bar, line, pie, radar, radial, scatter, heatmap, sparkline — is a small builder over one core item. chart ships the host that renders a chart, the eight-slot palette, the shared frame (axes, grid, color scale, legend, tooltip, focus), and the helpers the families share. Underneath it is TanStack Charts, which owns the scales, the scene, and the interaction.

You rarely install it on its own: pick a family and it brings the core along. Reach for the core directly when you compose a chart by hand, when you need the host's props, or when you want the values in chartDefaults.

Installation

npx shadcn@latest add @dotui/chart

Installing any family installs it too:

npx shadcn@latest add @dotui/chart-bar

Families

Each family is an independent item with its own curated set of variants. Browse them all on the charts page.

  • Area chart — magnitude and composition across an ordered domain.
  • Bar chart — categories compared by length, grouped, stacked, or horizontal.
  • Line chart — how a value moves across an ordered domain.
  • Pie chart — parts of a single whole, including donuts and rings.
  • Radar chart — a profile across a handful of categories.
  • Radial chart — bars bent around a circle; gauges and progress.
  • Scatter chart — two quantitative measures against each other.
  • Heatmap — one value per pair of categories, carried by color.
  • Sparkline — a chrome-free line beside a number.

Usage

A family component owns the whole chart: give it rows and the names of the fields to read.

import { AreaChart } from '@/ui/chart-area'

The core is what you import when you need the host, the defaults, or a helper:

import { Chart, chartDefaults, stackY } from '@/ui/chart'

The host

Chart renders a chart definition. Every family renders one, so its props are available on every family component:

<Chart
  definition={definition}
  ariaLabel="Visitors by month"
  height={220}
  className="w-full"
/>
  • ariaLabel is required — a chart is a figure, not decoration.
  • height defaults to 256. Pass aspectRatio to size by ratio instead, or width to opt out of measuring the container. Width is measured and tracked on resize; height never is.
  • className lands on the outer box — size and place the chart there.
  • children render as an HTML overlay above the surface, ignoring pointer events: center text on a donut, a hover readout, a watermark. The tooltip is portaled, so it always paints above the overlay.

The definition is compared by identity, and that identity is the update contract: build it at module scope, or memoize it. Family components do this for you.

Wide and long data

Two row shapes draw the same chart, and every cartesian family accepts both.

Wide rows carry one column per series. Pass the columns as an array and name them with labels:

const data = [
  { month: 'Jan', desktop: 186, mobile: 80 },
  { month: 'Feb', desktop: 305, mobile: 200 },
]
<AreaChart
  data={data}
  x="month"
  y={['desktop', 'mobile']}
  labels={{ desktop: 'Desktop', mobile: 'Mobile' }}
  ariaLabel="Visitors by device"
/>

Long rows carry one row per series and point. Name the splitting field with series, and fix the order with seriesOrder — it drives both the color slots and the legend:

const data = [
  { month: 'Jan', device: 'desktop', visitors: 186 },
  { month: 'Jan', device: 'mobile', visitors: 80 },
]
<AreaChart
  data={data}
  x="month"
  y="visitors"
  series="device"
  seriesOrder={['desktop', 'mobile']}
  labels={{ desktop: 'Desktop', mobile: 'Mobile' }}
  ariaLabel="Visitors by device"
/>

data is compared by identity, so define it outside your component — or memoize it. Every other prop is a flat scalar and can change freely.

Stacking

Stacking is a data transform, not a chart flag. stackY takes wide rows and returns one long row per band, carrying the interval to draw (base, top) plus the row's own value for tooltips and labels:

import { stackY } from '@/ui/chart'

const stacked = stackY(data, {
  x: 'month',
  y: ['desktop', 'mobile', 'other'],
})
<AreaChart
  data={stacked}
  x="x"
  y="top"
  y1="base"
  series="series"
  seriesOrder={['desktop', 'mobile', 'other']}
  ariaLabel="Visitors by device, stacked"
/>

Pass normalize to divide each group by its own total for a 100% stack. A null contribution leaves a gap instead of a zero-height band. Call stackY at module scope: its result is identity-compared like any other data.

Defaults

chartDefaults holds every house decision in one object — height, curve, stroke width, fill opacity, dot and bar radii, padding, focus mode, tooltip anchor, animation:

import { chartDefaults } from '@/ui/chart'

chartDefaults.height // 256
chartDefaults.curve // 'natural'
chartDefaults.focus // 'group-x'
chartDefaults.animate // { duration: 240, respectReducedMotion: true }

Read from it instead of repeating a literal, and change it there when you want a different house style — every family picks the change up. Charts above chartDefaults.animateMaxPoints (800 points) turn animation off on their own.

Marks

marks and marksBefore take raw TanStack Charts mark layers, painted over and under the family's own marks on the same scales. This is the escape hatch for anything the family props don't cover — a target rule, an end dot, value labels:

import { text } from '@tanstack/charts/text'

// Same `z` as the bars, so grouped focus keeps one tooltip row per month.
const labels = [
  text(data, {
    x: 'month',
    y: 'desktop',
    text: 'desktop',
    z: () => 'Desktop',
    fill: 'var(--color-fg-muted)',
    fontSize: 12,
    dy: -10,
  }),
]
<BarChart data={data} x="month" y="desktop" marks={labels} ariaLabel="…" />

Build mark arrays at module scope — they are compared by identity, so an inline array rebuilds the chart on every render.

Spec fragments

Each family also exports its spec builder — areaChartSpec, barChartSpec, pieChartSpec, and so on. A builder returns the chart spec the family would have rendered, so you can pass it to defineChart yourself and keep full control of the definition:

import { defineChart } from '@tanstack/charts'

import { Chart } from '@/ui/chart'
import { barChartSpec } from '@/ui/chart-bar'

const definition = defineChart({
  chart: (ctx) => barChartSpec({ data, x: 'month', y: 'desktop' }, ctx),
  focus: 'nearest',
})
<Chart definition={definition} ariaLabel="Visitors by month" />

The builder receives the library's build context, which re-runs inside every scene build: that is how tick density tracks the chart's width for free. For a chart with no family at all, compose chartFrame, planChart, and paletteGradients the same way the families do.

Theming

Series colors come from a categorical palette of eight slots, --chart-1 through --chart-8. Your design system derives them from its theme hues, so charts stay on-brand and adapt to light and dark automatically — no per-mode values to maintain. The palette is editable under Chart colors in the editor, and updates live on the charts page.

Series are assigned slots in order, so seriesOrder decides which series gets which color. To reach a slot yourself — a mark's fill, a stat card's accent — use the variable directly, or paletteColor, which wraps around the eight slots:

import { paletteColor } from '@/ui/chart'

paletteColor(0) // 'var(--chart-1)'
paletteColor(8) // 'var(--chart-1)'

var(--chart-N) works anywhere a color does, including gradient stops. Any CSS color works too, so a semantic mark — a threshold rule, a negative bar — can point at var(--color-fg-danger) instead of a palette slot.

Accessibility

  • ariaLabel is required and names the figure; add ariaDescription when the takeaway needs a sentence.
  • The chart surface is in the tab order. Arrow keys move between points, Home and End jump to the first and last, Enter and Space pin the tooltip, and Escape dismisses it.
  • Color alone never carries the distinction — the legend, tooltip, and axis labels all name the series.
  • Keep the surrounding context in normal HTML: a heading, the units and time range, and a table when exact values matter. It is easier to navigate, translate, and print than text inside an SVG.

API Reference

Chart

The chart host: it renders a chart definition, owns the SVG renderer and the default height, and layers `children` over the surface as a pointer-events-none HTML overlay. Every family component (`AreaChart`, `BarChart`, …) renders one.

PropType
string
string
number
ReactNode
ChartDefinition<unknown, ChartValue, number>
number
string
number
function
number

Interaction and animation props are shared by every chart family:

Interaction and animation props shared by every chart family component. They are flat scalars on purpose: the chart definition is memoized on a serialized key, and a nested option object would silently go stale.

PropType
ChartFocus
number
ChartTooltipAnchor
boolean
false
ChartAnimate

Last updated on 8/1/2026