Chart
ProA dependency-free plotting primitive for trends, funnels and pies. Compose the named parts into one measured SVG; the legend focuses and toggles series, and every hue re-themes.
Installation#
Usage#
Chart is composed from named parts, like Card. The root takes the data, an optional index key for category labels, and a config that names each series’ label and color. The parts (ChartGrid, ChartArea, ChartLine, ChartTooltip) read it from context and draw into one shared, pixel-measured SVG.
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
import {
Chart,
ChartLegend,
ChartGrid,
ChartXAxis,
ChartYAxis,
ChartArea,
ChartLine,
ChartReferenceLine,
ChartAnnotation,
ChartTooltip,
} from "@/components/ui/chart"
<Chart>
<ChartLegend />
<ChartGrid />
<ChartXAxis />
<ChartYAxis />
<ChartArea />
<ChartLine />
{/* or ChartBar, ChartBars, ChartAreas, ChartFunnel, ChartPie */}
<ChartReferenceLine />
<ChartAnnotation />
<ChartTooltip />
</Chart>Load animation#
Every chart plays a one-shot reveal the first time it mounts: the ChartLine self-draws left to right, the ChartArea fill rises from the baseline, and each ChartBar grows up from the zero line in a left-to-right cascade. It fires once, never on hover or re-sort, and honors prefers-reduced-motion (every layer simply appears drawn). Pass animate={false} on the Chart root to opt out. Press Replay to watch it again.
Line#
Layer a ChartLine per series; each resolves its hue from config, or takes an explicit color. The shared ChartTooltip lists every series at the hovered category, and a crosshair tracks the active point.
Area#
ChartArea fills the space under the trend with a currentColor gradient that fades to the baseline, so the wash is just the series hue at low opacity: a blue line yields a soft blue-100/200 fill. Pair it with a ChartLine for a crisp edge.
Smooth curves#
Pass curve="monotone" to ChartLine, ChartArea or ChartAreas to join the points with a monotone cubic instead of straight segments. It is smooth, yet it still passes through every point and never swings above a peak or below a trough the data does not have: a plateau stays flat and an extreme sits exactly on its point, so the curve never invents a value. Gaps, stacking, dashed, markEnd, the draw-in and the hover dot all follow the curve. Set the same curve on the area and the line drawn over it.
Missing data#
A point with a non-numeric (or absent) value is a gap. By default the ChartLine and ChartArea break at the gap, so a dropout reads honestly. Pass connectNulls to bridge across it instead.
Bar#
ChartBar draws a bar per category from a 0-anchored baseline. Each bar stands square on the baseline and rounds only at its value end (the bottom, for a negative bar), and ChartGrid draws the zero line a step stronger, so the bars stand on a floor. Hovering a column brings it forward and dims the rest, so the active value reads at a glance.
Value labels & highlight#
Pass label to ChartBar to print each value just past its bar end. It returns inline content, so a figure can carry a glyph. To single one datum out, draw the series in neutral (the foreground at 10% on every theme) and pass its index to highlight, so that one bar takes full ink. Hovering a column also lifts its x-axis tick to the foreground.
Highlight & active bar#
highlight on ChartBar paints chosen bars in highlightColor (primary, full ink, by default) and leaves the rest in the series hue: an index for today, a list, a predicate over the row, or "active" for the bar under the pointer. The chart reports that bar through onActiveIndexChange (and takes a controlled activeIndex), so a legend or a headline can follow it. Here the latest month is lit until another is hovered. Swapping the highlight recolors the bars in place, so the grow-in never replays, and the tooltip swatch takes the hue the bar is wearing. While a column is hovered the others dim, except neutral bars: already faint, they hold their ink and the hovered one steps up instead, so no bar ever disappears.
Grouped & stacked bars#
ChartBars draws several bar series in one band. By default they render grouped side by side; set stack on Chart to stack them, and the y-axis grows to the per-category total automatically. Grouped series rotate distinct hues; stacked ones take shades of one blue, since their order already tells them apart. A color in config always wins.
Stacked area#
ChartAreas stacks several area series into a band chart when stack is set on Chart: each series sits on the cumulative total below it as a translucent band under its own edge line. A stack reads by its order, so series that name no color take shades of the house blue, dark at the floor to light at the top (palette="tonal", the default whenever stack is set). Without stack the series overlap from the baseline.
Legend#
Put ChartLegend inside the Chart and it keys the chart’s own series, sits above the plot (or below it, with placement="bottom") and becomes the chart’s control surface, as in Mixpanel. Hover a key and its series comes forward while the rest recede; click it to hide or show the series, and the scale and the stack refit around what is left; Alt+click shows it alone. The last series showing never hides. Past max keys, the rest fold into a +N more menu with the same controls.
Outside a chart, ChartLegend is a plain key: feed it the same config (or explicit items) and each swatch resolves its hue exactly like the chart, palette fallback included.
Give an item a value and it prints after the label in the foreground, so the key doubles as a summary of each series. Inside a chart, value="sum", "average" or "last" summarises each series from the data, in its own format.
Driving it from outside#
The series state is controllable, so a table under the chart can run it, the way Mixpanel pairs every insight with its breakdown. hiddenSeries with onHiddenSeriesChange takes the checkboxes, and focusedSeries with onFocusedSeriesChange lights the plan under the pointer. A series and the period it is compared against always stay lit together.
Dual axis & per-series format#
Give a series axis: "right" in config to scale it against a secondary y-axis (so two metrics at different magnitudes share one chart), and render a second ChartYAxis with side="right". Each series can also carry its own format, so the tooltip shows, say, currency for revenue and a plain count for orders.
Forecast & end marker#
ChartLine takes dashed for projected data and markEnd to pin a filled dot on the last point (a “current value” marker). Split actuals and projection into two series that share the seam point, and the solid line meets its dashed continuation cleanly.
Incomplete period#
Today’s count is still climbing, so drawn like a finished day it reads as a collapse. Pass partial on Chart (or a number, for several trailing periods) and the open period draws as provisional, Mixpanel’s treatment: a line dashes from the last complete point on the exact same curve, its area fades, a bar hatches, and the tooltip marks the period with partialLabel.
Compare to previous period#
Name the earlier period in a series’ config with compare and the pair reads as one story. The earlier series becomes a ghost: it borrows the hue, takes no palette slot, and its ChartLine draws thin, faint and dashed with no prop set. The tooltip prints the change beside the current value, green up and red down; give a down-is-good metric (churn, refunds, latency) inverted and the colors flip. In ChartBars the ghost stands beside its partner, faint.
Reference lines#
ChartReferenceLine draws a horizontal threshold (y) or vertical marker (x) across the plot, with an optional label. Compose it after the data so it annotates on top, for a target, limit, or average. It defaults to a muted dashed line; pass a color to emphasize it.
Annotations#
ChartAnnotation pins a note to a category: a launch, an outage, a release, so the jump in the line explains itself. Its marker sits in a lane above the plot (the plot steps down to make room on its own) with a faint rule down to the baseline, and hovering it opens the note. Mixpanel numbers its markers; pass any short label, or none for a dot, and a color for a note that is bad news. The text also reaches screen readers beside the data table.
Tooltip rows#
footer on ChartTooltip adds rows under the series, below a hairline, for a figure the chart does not draw: here the net of income and expenses. Build each row with ChartTooltipItem (a swatch, a label, a value) so it lines up with the rows above and its swatch keeps its hue in the portaled bubble. labelFormatter receives the whole row and its index after the label, so the heading can say more than the axis (a full month name).
Tooltip order & totals#
With several series, the tooltip reads best as a ranking. sort="desc" orders the rows by their value at the hovered category, so the leader comes first however the lines cross (a comparison row stays under its series). total adds the column’s sum below a hairline (a string names it), and maxItems folds the tail into a +N more line. While a legend key holds the focus, the other rows recede with their series.
Axis labels that fit#
interval="auto" on ChartXAxis measures its labels against the plot’s width and skips just enough of them that none collide, re-thinning as the chart resizes. It counts back from the last category, so the latest day or month always carries its name. minTickGap sets the clear space between labels (12px), and minInterval keeps a month of days reading by the week however wide the plot. A number still labels every Nth category from the first.
Axis titles & compact figures#
ChartYAxis prints compact figures by default (12K, 1.4M), the way Mixpanel and Attio label an axis; pass tickFormatter for currency or units. Both axes take a label: the y title turns to run along the outer edge, the x title centers under the ticks, as in Attio’s report builder. Give the chart the room for them in padding.
Negative values#
The domain anchors to zero, and bars and areas grow from the value baseline (the y-zero line), so positive values rise above it and negative values drop below. No extra config: pass data that crosses zero and it reads correctly.
Fitted domain#
A full chart starts its y-axis at zero, which is right for bars and totals. A balance or a rate moves in a band far from zero, and a zero floor flattens it into a line. Pass domain="fit" and the axis wraps the data’s own extent in nice rounded bounds, with ticks on a nice step, so the grid and the labels still land on round values. It never crosses zero when the data does not. A [min, max] pair still fixes the domain exactly.
Funnel#
ChartFunnel draws conversion through ordered steps, as Mixpanel and Attio do. Each step is a column, and above it a faint ghost rises to the step before, so what dropped off reads as the empty part of the column. Each step carries its conversion from the first, and ChartTooltip adds the conversion and drop-off rows by itself. Give the chart about 32px of padding.top for the first label.
Pie & donut#
ChartPie draws each row as a slice, clockwise from twelve o’clock, with a clean gap between slices. innerRadius sets the hole (0.62 by default, a donut; 0 for a pie), and its children center in the hole. Hovering a slice lifts it out a step while the rest recede, with its value and share in the tooltip. A ChartLegend in the same chart keys the slices in their hues and lights them from the key.
Category bar#
CategoryBar breaks one whole into its parts in a single line: credits by tool, storage by file type, time by project. It takes the same rows as a pie (each one a category, named by index, sized by dataKey), so where a donut spends a square, this sits under a figure or across a card. Each segment’s width is its share, with a clean gap between segments, and CategoryBarLegend keys them underneath with the share after each name. Hovering a segment or a key lights it and recedes the rest, with its amount and share in the tooltip. Give the long tail’s Other the gray hue.
size sets the thickness (6, 8 or 12px), palette="tonal" paints shades of the house blue instead of distinct hues, and the legend’s value prints the share, the amount (through valueFormatter), both, or nothing.
Sparkline#
Pass sparkline for a chromeless trend: zero padding (it bleeds to the edge), points spanning the full width, and currentColor so a single text-* utility sets the hue. This is exactly what StatSparkline renders under the hood.
Sparkline bars#
A sparkline can draw bars too, for a week of orders beside a figure. Once a ChartBar is in it, the categories sit in bands (the first and last bars keep their full width) and the scale runs from zero, so the shortest bar still reads as a bar. Add ChartTooltip for a hover readout, and highlight to light today.
Colors#
Every series takes a semantic hue: blue (the default), teal, purple, orange, pink, the status roles, or current to inherit the ambient color. Series without a color follow the chart’s palette: categorical rotates distinct hues led by blue, for lines that cross and must be told apart; tonal spreads the blue ramp (blue-800 to blue-200) as far apart as the count allows, and is the default for a stack.
Loading & empty#
Pass loading to render a Skeleton in place of the plot (it also sets aria-busy). When data is empty, the chart shows a muted message; override it with the empty prop.
Accessibility#
The SVG is decorative (aria-hidden): pixels do not read well to a screen reader. Instead, every full chart renders a visually-hidden data table as its accessible equivalent, so assistive tech gets the real numbers, row by row. Pass label to name the chart, it becomes the table’s caption. loading sets aria-busy. Sparklines stay decorative (no table), since they are labelled by their surrounding context, like a Stat’s value.
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
API reference#
Chart
The root: measures itself, builds the scales and shares them with every part, which draw into one SVG. Renders a visually hidden data table for screen readers. Forwards all native <div> props, ref included. Every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| data* | ChartDatum[] | number[] | - | A row per category, or a bare number[] for a single value series. null marks a missing point. |
| index | string | - | The key holding each row's category label, for the x-axis and tooltip heading. |
| config | Record<string, ChartSeriesConfig> | - | Per series: label, color, format, axis ("left" | "right"), compare (the key it is measured against) and inverted (down is good). A ChartColor is a semantic hue (blue, teal, purple, brand, gray, a status role, blue-200 to blue-800) or current. |
| domain | "auto" | "fit" | [number, number] | "auto" | auto runs from zero to a nice top; fit wraps the data's own extent; a pair fixes it. |
| sparkline | boolean | false | Chromeless: zero padding, points across the full width, painted in currentColor. |
| stack | boolean | false | Stacks ChartBars and ChartAreas, and sizes the domain to the category total. |
| palette | "categorical" | "tonal" | "categorical" ("tonal" with stack) | How series without a color are painted: distinct hues, or shades of one blue. |
| padding | Partial<{ top: number; right: number; bottom: number; left: number }> | - | The plot insets, in px. Raise bottom and left to make room for axes. |
| animate | boolean | true | The one-shot load reveal. Held under reduced motion. |
| crosshair | boolean | true (false for sparklines) | The hover tracking line, painted behind the data. |
| activeIndex | number | null | - | The hovered category, controlled. |
| onActiveIndexChange | (index: number | null) => void | - | Called as the pointer crosses categories, and with null when it leaves. |
| hiddenSeries | string[] | - | The series switched off, controlled. They leave the scale, stacks and tooltip. |
| defaultHiddenSeries | string[] | - | The series switched off at first, uncontrolled. |
| onHiddenSeriesChange | (keys: string[]) => void | - | Called when a series is shown or hidden. |
| focusedSeries | string | null | - | The series in focus, controlled; the rest recede. |
| onFocusedSeriesChange | (key: string | null) => void | - | Called when the focused series changes. |
| partial | boolean | number | - | Trailing categories still in progress (true is the last): lines dash, areas fade, bars hatch. |
| loading | boolean | false | A Skeleton in place of the plot, with aria-busy. |
| empty | React.ReactNode | "No data" | Shown when data is empty. |
| label | string | - | The accessible name; it also captions the hidden data table. Strongly recommended. |
ChartLine
A line series. Forwards SVG <path> props except d.
| Prop | Type | Default | Description |
|---|---|---|---|
| dataKey | string | "value" | The series to plot. |
| color | ChartColor | - | Overrides the hue from config. |
| curve | "linear" | "monotone" | "linear" | Straight segments, or a smooth curve that never overshoots. Match the area under it. |
| connectNulls | boolean | false | Bridges missing points instead of breaking the line. |
| dashed | boolean | false | A dashed line for projected data; skips the draw-in. |
| markEnd | boolean | false | A filled dot on the last point. |
| variant | "default" | "comparison" | - | comparison is the faint dashed ghost of a previous period, the default for a series another one compares against. |
| hideActiveDot | boolean | false | Hides the dot on the hovered point. |
| strokeWidth | number | - | The stroke width. |
ChartArea
An area series fading down from its edge; pair it with a ChartLine on top. Forwards SVG <path> props except d.
| Prop | Type | Default | Description |
|---|---|---|---|
| dataKey | string | "value" | The series to fill. |
| color | ChartColor | - | Overrides the hue from config. |
| curve | "linear" | "monotone" | "linear" | How the top edge joins its points. |
| connectNulls | boolean | false | Bridges missing points instead of leaving a hole. |
ChartAreas
Several area series at once, stacked into bands with stack on the root, each with its own edge line. Forwards SVG <g> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| keys | string[] | - | The series to draw, bottom of the stack first. Defaults to every series. |
| curve | "linear" | "monotone" | "linear" | How every band's edges join their points. |
| connectNulls | boolean | false | Bridges missing points instead of leaving a hole. |
ChartBar
A single bar series. Forwards SVG <path> props except d.
| Prop | Type | Default | Description |
|---|---|---|---|
| dataKey | string | "value" | The series to draw. |
| color | ChartColor | - | Overrides the hue from config. |
| barRatio | number | 0.62 | Bar width as a fraction of the category band. |
| radius | number | 8 | Corner radius at the value end, in px. |
| label | (value: number, index: number) => React.ReactNode | - | A value label past each bar's end. Give the Chart about 24px of padding.top. |
| highlight | number | number[] | "active" | ((datum, index, active) => boolean) | - | Bars painted in highlightColor: an index, a list, the hovered bar, or a predicate. |
| highlightColor | ChartColor | "primary" | The hue of a highlighted bar. |
ChartBars
Several bar series sharing each band: grouped, or stacked with stack on the root. Forwards SVG <path> props except d.
| Prop | Type | Default | Description |
|---|---|---|---|
| keys | string[] | - | The series to draw, in order. Defaults to every series. |
| barRatio | number | 0.7 | Group width as a fraction of the category band. |
| radius | number | 8 | Corner radius at each bar's value end; a stack rounds its outer segment only. |
ChartGrid
Horizontal grid lines on the y ticks. Forwards SVG <g> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| rows | number | 4 | The number of horizontal bands. |
| showVertical | boolean | false | Also draws a line per category. |
| baseline | boolean | true | Draws the zero line a step stronger, when zero is in the domain. |
ChartXAxis
The category labels under the plot. Forwards SVG <g> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| tickFormatter | (value: string | number, index: number) => string | - | Formats a category label. |
| interval | number | "auto" | 1 | Labels every Nth category, or auto: thins by measuring, counted back from the last category. |
| minTickGap | number | 12 | With auto: the minimum space between labels, in px. |
| minInterval | number | 1 | With auto: never label more often than every Nth category. |
| label | string | - | An axis title under the labels. Give the Chart about 24px more padding.bottom. |
ChartYAxis
The value ticks, in compact figures (12K, 1.4M) by default. Forwards SVG <g> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| count | number | 4 | The number of ticks. |
| tickFormatter | (value: number) => string | - | Formats a tick, e.g. as currency. |
| side | "left" | "right" | "left" | right labels the secondary scale on the right edge. |
| label | string | - | An axis title along the outer edge. Give the Chart about 20px more side padding. |
ChartReferenceLine
A threshold (y) or marker (x) across the plot; compose it after the data. Forwards SVG <line> props except x and y.
| Prop | Type | Default | Description |
|---|---|---|---|
| y | number | - | Draws a horizontal line at this value. |
| x | number | - | Draws a vertical line at this category index. |
| label | string | - | A label at the line's end. |
| variant | "dashed" | "solid" | "dashed" | The stroke. |
| color | ChartColor | - | The hue; a muted neutral by default. |
ChartTooltip
The hover bands and the shared bubble, a row per series. Takes only the props below.
| Prop | Type | Default | Description |
|---|---|---|---|
| valueFormatter | (value: number) => string | - | Formats each value. A series' own format wins. |
| labelFormatter | (value: string | number, datum: ChartDatum, index: number) => React.ReactNode | - | Formats the heading from the category, its row and index. |
| footer | (datum: ChartDatum, index: number) => React.ReactNode | - | Extra rows under a hairline; build them with ChartTooltipItem. |
| sort | "none" | "desc" | "asc" | "none" | Orders the rows by value at the hovered category. |
| total | boolean | string | - | Adds a total row ("Total", or your label). |
| maxItems | number | - | Folds the rows past this count into "+N more". |
| partialLabel | React.ReactNode | "In progress" | The heading of a partial category. |
| className | string | - | Merged onto the bubble. |
ChartTooltipItem
One tooltip row: swatch, label, right-aligned value. Works in footer and outside a Chart. Forwards native <span> props except children.
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | React.ReactNode | - | What the row measures. |
| value* | React.ReactNode | - | The figure. |
| color | ChartColor | - | The swatch hue; omit for no swatch. |
| change | number | null | - | A change printed before the value, as a fraction (0.12 is "+12%"). |
| inverted | boolean | false | Down is good: a fall prints green. |
| ghost | boolean | false | A comparison row with a faint swatch. |
| dimmed | boolean | false | Recedes the row. |
ChartLegend
The key. Inside a Chart it lists the series (or slices) above the plot and focuses, hides and isolates them; outside, a plain key. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| config | ChartConfig | - | Outside a chart: entries from the same config the Chart takes. |
| items | { label: string; color?: ChartColor; value?: React.ReactNode; dataKey?: string }[] | - | Explicit entries; win over config and the chart's series. |
| align | "start" | "center" | "end" | "start" | The alignment of the row. |
| placement | "top" | "bottom" | "top" | Inside a chart: above or below the plot. |
| interactive | boolean | true | Hover focuses, click toggles, Alt+click isolates. false for a static key. |
| max | number | - | Folds the keys past this count into a "+N more" menu. |
| value | "last" | "sum" | "average" | ((key: string) => React.ReactNode) | - | A figure after each label, summarised from the data. |
| Data attribute | Description |
|---|---|
| data-hidden | On a key whose series is switched off (it also gets aria-pressed="false"). |
ChartAnnotation
A note pinned to a category: a marker in a lane above the plot that opens on hover. Forwards SVG <g> props except title.
| Prop | Type | Default | Description |
|---|---|---|---|
| index* | number | - | The category the note belongs to. |
| title* | React.ReactNode | - | The headline of the note. |
| label | React.ReactNode | - | What the marker shows (a number, a letter); omit for a dot. |
| description | React.ReactNode | - | More detail under the title. |
| color | ChartColor | - | The marker hue; the foreground by default. |
ChartFunnel
Ordered steps as columns, each with a ghost of the drop-off and its conversion from the first. Forwards SVG <path> props except d.
| Prop | Type | Default | Description |
|---|---|---|---|
| dataKey | string | "value" | The count at each step. |
| color | ChartColor | - | The column hue. |
| barRatio | number | 0.62 | Column width as a fraction of the band. |
| radius | number | 8 | Top corner radius, in px. |
| label | false | ((value: number, index: number, conversion: number) => React.ReactNode) | - | The label over each step; the conversion by default, false for none. |
ChartPie
A pie or donut, one slice per row named by the Chart's index. Forwards SVG <g> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| dataKey | string | "value" | The value each slice is sized by. |
| innerRadius | number | 0.62 | The hole as a fraction of the radius; 0 draws a pie. |
| gap | number | 2 | Space between slices, in px. |
| colors | ChartColor[] | - | Slice hues in row order; the palette by default. |
| valueFormatter | (value: number) => string | - | Formats the value in the slice tooltip. |
| tooltip | boolean | true | A tooltip per slice with its value and share. |
| children | React.ReactNode | - | Content centered in a donut's hole. |
CategoryBar
One whole split into segments in a single bar, standalone (no Chart). Forwards native <div> props except children.
| Prop | Type | Default | Description |
|---|---|---|---|
| data* | ChartDatum[] | - | One row per category. |
| index | string | "label" | The key holding each category's name. |
| dataKey | string | "value" | The key holding each amount; shares come from the total. |
| colors | ChartColor[] | - | Segment hues in row order. Give "Other" gray. |
| palette | "categorical" | "tonal" | "categorical" | The hues for categories colors leaves out. |
| size | "sm" | "md" | "lg" | "md" | The track height: 6, 8 or 12px. |
| valueFormatter | (value: number) => string | - | Formats an amount in the tooltip and legend. |
| tooltip | boolean | true | A tooltip per segment: name, amount and share. |
| animate | boolean | true | Sweeps the bar open on mount. Held under reduced motion. |
| label | string | - | What the bar breaks down; leads its accessible name. |
| children | React.ReactNode | - | Parts under the bar: a CategoryBarLegend. |
CategoryBarLegend
The key under a CategoryBar; hovering a key recedes the other segments. Forwards all native <ul> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | "share" | "value" | "both" | "none" | "share" | What follows each name. |