Skip to main content

BarChart

Comparisonchart

Categorical comparison — composed from Bar + BarXAxis/BarYAxis inside its provider.

Bar chart, 3 series over EMEA–LATAM; renewal peaks at 1,007 in EMEA
import { BarChart } from "@elabs-ai/components-charts";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui BarChart chart (from @elabs-ai/components-charts) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look BarChart up before writing code: `npx -y @elabs-ai/components-cli docs BarChart` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-charts is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Use it for

  • How do these compare? A few categories, compared.
  • categorical comparison of one or more measures across a small set of named categories
  • a single signed measure around a meaningful zero, as diverging bars with a zero line

Avoid

  • a time axis with many points — use a line or area chart
  • Truncating the value axis so small differences look large — a bar axis starts at zero.
  • Hardcoding series colors — use the chart-1…chart-5 tokens so every theme works.
  • Rendering an empty frame before data arrives — use the shipped chart skeleton utilities.

Works with

Contains

Examples

39 live examples

Default

Default grouped bar chart with two series and a tooltip.

Showcase

Everything the parity waves added to `BarChart`, composed on one realistic chart: stacked segments ordered largest-first with a total past each bar (`stacked` + `stackOrder="desc"` + `showTotals`), rows sorted by that total (`sort="desc"`), a toggleable container legend naming the two series (`legend={{ interactive: "toggle" }}`), a pinned, compact-formatted value axis (`YAxis domain`/`title`), and long region names that wrap onto two lines rather than tilt. `ChartFrame` adds the title, description, notes, source and export chrome around it. Left out: the placement-`"auto"`/hover-only value-label mode (`labels={{ placement: "auto" }}` / `{ visibility: "hover" }`) doesn’t read here — a stacked layout always centres a segment’s label regardless of placement, so the two features conflict, and the always-on centred label reads better on a totals chart.

Single Series

Single-series bar chart — simplest usage.

Stacked

Stacked bar chart — segments are stacked on the same bar.

Horizontal

Horizontal orientation — bars grow left-to-right.

Hairline Hatch

THE HAIRLINE SEAM — `fillStyle="hatch"`. One solid lead, and the series it is measured against drawn as OUTLINED hairline hatch in their own colours: the comparison recedes without losing its hue. It is an author's choice, so it holds at every decoration level (the automatic pattern swap at decoration 8–10 is a different mechanism, for telling series apart without hue). The `Ruler` mark gives the card-sized plot a sense of measure with no axis; the values are printed on the bars instead (`labels`).

Horizontal Long Labels

`BarYAxis maxWidth`: rows whose names ARE the content. The label gutter is measured and capped at 112 px by default, which ellipsises "Send Invoice → Payment Reminder"; raising the cap lets the gutter grow to what the labels need, and the plot keeps its own minimum.

Long Category Labels

Long labels that cannot fit their band tilt to 45° and ellipsize to the band the chart reserved for them. The full name stays available to assistive tech via an `sr-only` twin, so "Q1 Wester…" is never what a screen reader reads.

Many Categories

Forty categories in 560px: no band is wide enough for its label and none is wide enough to tilt into, so the axis strides over categories and re-enters horizontally at the wider effective slot.

Narrow Container

The same long labels in a card-sized column — the cascade trims harder.

Tiny Container

Below the legibility floor the axis hides itself rather than shipping a row of overlapping fragments. The bars still read; the category names move to the tooltip and to the chart's accessible description.

Fit "off" (escape hatch)

`fit="off"` pins the pre-fit render — full labels, count-capped stride, no measurement and no reserved band. The escape hatch for a chart whose layout was tuned against the old behaviour.

Horizontal With Category Axis

Horizontal bars with the side category axis. A side axis never tilts — rotated row labels are a legibility regression — so it ellipsizes into a gutter the chart widens to fit, instead of clipping at a constant 70px.

Loading

Loading vs ready (#268) — placeholder categories/bars while `status="loading"`.

Drilldown

Click a bar to drill into it; the detail panel below reports the datum.

Keyboard Drilldown

The same drill-down reached with the keyboard only — one tab stop for the whole chart, ArrowRight to traverse, Enter to activate.

Density comparison

#394: the axis tick label reads the `text-meta` ROLE (was the raw `text‑xs` utility, which `data-density`/#340 cannot reach). Two columns pin `data-density` on a plain wrapping div — NOT `<ThemeProvider>`, which writes `data-density` to `document.documentElement` and would race two columns — mirroring `Foundations/Typography → Density scale`'s pattern. Must match `Gantt`'s already-density-aware timescale tick (11.25px compact / 12px comfortable / 12.75px spacious, styling-and-tokens.md).

With Values

`labels` prints each bar's value as a `HaloText` label (the `text-chart-value` role) — lieflat G3 Chunky Bars. `animate={false}` settles the labels immediately for a stable story.

Unit Rungs

`unit` draws each bar as a countable `UnitStack` of `floor(value / unit)` rungs instead of a solid fill — lieflat F1 Rung Bars. Renders instantly (no grow-in). The rung pitch comes from the value scale (not the bar's own pixel span), so one rung is worth the same amount in every column, and the ladder never counts past the value it encodes (#241).

Diverging

Negative values draw BELOW the zero baseline (lieflat G10 Diverging Bar): capsule radius on the outer end only, and the zero hairline auto-shows because at least one value is negative.

Diverging — high decoration

#254: `Diverging` pinned to high decoration — the render path `Diverging`'s own lock cannot see. At `--decoration` 8-10 `bar.tsx` swaps the palette fill for a `url(#bp-series-…)` pattern (ADR 0011); this story proves that swap actually fires (not merely that a selector tolerates it) so the broadened `data-slot` selector above is verified on both render paths, not just one.

Highlight

`highlightKey` marks ONE bar as the series' "hero" — it draws in `--chart-foreground` ink while every other bar draws from `palette` instead of `fill` — lieflat's one-hero-bar-in-ink convention.

Mono Palette

Nine unfilled `Bar` series exceed the categorical palette's 6-series soft cap (RM-018), so `resolvePalette` auto-degrades to the neutral mono ladder and logs a dev-only warning — a naive multi-series chart never silently paints two series the same colour.

Reveal In View

Three stacked `BarChart`s with `revealOn="inView"`. Only a chart that has actually scrolled 30% into view grows its bars; the ones below the fold stay held (no timer lapses them into "ready" off-screen). Under reduced motion nothing is held — a below-the-fold chart shows its bars without scrolling.

Replay On Click

`replayOnClick`: clicking the chart body replays the bar grow; the button above is the keyboard equivalent. Under reduced motion a click does not put the grow back on screen.

Selection states

`selectionStates` paints the host's tri-state: selected bars carry a compound foreground/background outline, excluded bars dim AND carry a full-opacity dashed frame, so the three states stay distinguishable in greyscale.

Likert diverging stack

Likert rows as a diverging stack: `stacked="diverging"` with `divergingCenter="Neutral"` centres the neutral answers on the zero line; disagreement grows left, agreement right. Sign is carried by side AND by the value labels, never by hue alone.

Percent stack

The same answers as `stacked="percent"`: every category normalised to 100 %, the value axis reads 0–100 % and each segment prints its share.

Stacked with totals

`stacked` + `stackOrder="desc"` + `showTotals`: largest segment first, the total past each stack.

Range overlays, grouped

The range-bar recipe: two range overlays (light 90 %, dark 50 %) and an average tick per model, grouped by class with a bold header per group. The overlays are listed in the legend items the chart exposes.

Sorted, coloured by region

`sort="desc"` + `colorBy={{ key: "region" }}`: value order, one hue per region, and a key.

Comparison columns

`comparison={{ key: "prev" }}`: last year as a muted column behind each quarter, grey differences on top.

Track bars

`track`: a grey bar to the axis maximum behind each bar — "what is missing".

Signed horizontal

The signed reading: negative bars grow left from the zero line and every label sits at its bar's end, signed with a real minus.

Values on hover

`labels={{ visibility: "hover" }}`: a value label only for the hovered category.

Legend toggle

Container legend (RM-118): `legend={{ interactive: "toggle" }}` mounts `ChartLegend` above the plot with real `aria-pressed` buttons — click, or Tab then Enter, hides a series. The value domain recomputes from the visible series only, and stays zero-based (`resolveBarValueDomain`, charts-honesty). Works the same on a grouped or a `stacked` chart.

Legend toggle, stacked

`interactive: "toggle"` on a STACKED bar chart, at the chart's DEFAULT `animationDuration` (1100ms) — deliberately not zeroed out (RM-118 fix round 1, item 3: "not exercised live"), so this story runs the real non-zero-duration path a validator drives live in a browser, not the `animationDuration={0}` path every unit test uses. Every assertion after a state change goes through `waitFor` rather than a synchronous `expect` right after the click/keypress — the bar-enter tween on mount and the toggle's own stacked-segment tween both run for real here, so reading DOM geometry synchronously would race the animation instead of asserting its settled result. `BarChart`'s toggle recompute (`lines`, `bar-chart.tsx`) isn't gated by the reveal timer the way `LineChart`/`AreaChart`'s shared `time-series-chart-shell` used to be (see that shell's "recomputes even when toggled mid-reveal" test and `line-chart.stories.tsx`'s "Legend toggle" story, RM-118 validator FAIL 1a round 2), but this story still exercises the toggle against the real timer end to end rather than assume it.

Legend yields to the colorBy key

One key per chart (R4): RM-113's `colorBy` key already names every bar's colour, so the container legend engine yields — `legend` is set, but nothing new mounts, and there is no toggle affordance in this mode.

Lived-in: sales desk (average, category strip, range → frame copy)

A sales desk's ranking as it ships inside a `ChartFrame`: twenty territories, the network average as a computed rule, a category strip that appears only because the rows overflow `maxVisibleItems` (`scrollbar="auto"`), and a range gesture whose intents feed a local selection driver — the frame's description turns into the selected group's share. The selection toolbar sits in the frame's action row beside the table flip and CSV export.

API

BarChart

PropTypeDescription
datarequiredRecord<string, unknown>[]Data array - each item should have an x-axis key and numeric values
xDataKeystringKey in data for the categorical axis. Default: "name"
marginnumber | Partial<Margin>Chart margins: one number for every side, or per side. Default: 40 on every side.
animationDurationnumberAnimation duration in milliseconds. Default: 1100
animationEasingstringCSS easing for bar grow transitions.
enterTransitionTransitionMotion enter transition (spring or cubic-bezier tween).
revealSignaturestringSignature of motion URL state — triggers enter replay when it changes.
revealOnChartRevealOnWhen the enter reveal is allowed to play (#175). `"mount"` (default) plays as soon as the chart renders — no change from today. `"inView"` holds the bars at their pre-enter state until this chart's own container scrolls to 30% visible.
replayOnClickbooleanClicking the chart body replays the enter reveal (#175). Default `false`.
aspectRatiostringAspect ratio as "width / height". Default: "2 / 1"
plotHeightResponsive<ChartPlotHeight>The plot's own height (ADR 0039): px, or `{ aspect }` (width ÷ height), optionally per breakpoint. Wins over `aspectRatio`, which stays an alias.
classNamestringAdditional class name for the container
statusChartStatusLoading vs ready — shows skeleton chrome + placeholder bars while `"loading"`. Default: `"ready"`.
loadingLabelstringCentered shimmer label while loading.
barGapnumberGap between bar groups as a fraction of band width (0-1). Default: 0.2
barWidthnumberFixed bar width in pixels. If not set, bars auto-size to fill the band.
orientationBarOrientationBar chart orientation. Default: "vertical"
stackedBarStackedStack bars instead of grouping them. `true` stacks raw values; `"percent"` normalises each category to 100 % (value axis 0–100 %, a `YAxis` without its own format prints percent, `labels` prints shares); `"diverging"` centres `divergingCenter` on the zero line with the series declared before it growing left/down and those after it right/up (Likert rows). Default: false
stackGapnumberGap between stacked bar segments in pixels, in every `stacked` mode. It is cut out of the boundaries between segments only, half from each side, so every stack still starts on the baseline and ends at its total. A `Bar`'s own `stackGap` overrides it for that series. Default: 0
divergingCenterstring`stacked="diverging"`: the series straddling zero (e.g. `"Neutral"`). Unset: the series split in half.
stackOrderBarStackOrderSegment order inside each stack (`stacked`/`"percent"`). Default: `"data"` (declaration order).
showTotalsbooleanPrint each stack's total just past its end. Default: false
sortBarSortRow order: by value (`"asc"`/`"desc"`, the stack total when stacked) or `{ by, dir }`. Default: `"none"`.
reversebooleanReverse the (sorted) row order. Default: false
groupBystringGather rows by this column: horizontal bars get a bold header row per group, vertical columns a header above each group; groups are separated by a hairline and keep their own (sorted) order.
colorByChartColorByColour each bar by another column through `resolvePalette` (six hues, then the neutral ladder with a dev warning) and show a colour key. One key per chart (RM-118 R4): whenever this produces a non-empty colour key, the `legend` container legend below YIELDS and renders nothing — `colorBy`'s own key already covers the same job, and `interactive: "toggle"` has no effect in that mode (there is no container legend to press).
trackboolean | { fill?: string }Paint a track behind each bar to the axis maximum ("to 100 %"). `true` uses `--chart-mono-2`, a mark-weight grey; `{ fill }` names another ink — a surface tone such as `var(--chart-segment-background)` when the track should read as paper, not as data. Default: false
overlaysBarOverlay[]Value markers and range spans drawn per bar on top of the series; listed in `legendItems`.
comparisonBarComparisonA muted prior-period column behind each main column (the main column narrows to make room).
comparisonLabelBarComparisonLabelGrey label beside each comparison pair. Default: `"none"`.
childrenrequiredReactNodeChild components (Bar, Grid, ChartTooltip, etc.)
onPhaseChange(phase: ChartPhase) => voidReports reveal lifecycle for OG screenshots and loading orchestration.
onDatapointClickChartDatapointClickHandlerDrill-down (#349). Fires when a bar is activated by pointer OR keyboard. Setting it mounts a keyboard-operable target layer OUTSIDE the aria-hidden SVG — one tab stop, arrow keys to traverse. Unset changes nothing.
copyValueOnActivatebooleanPut the datapoint's exact value on the clipboard when it is activated — the recovery path for a compact axis label. Default `false`; a chart with no interaction props still renders byte-identical DOM. A consumer-supplied `onDatapointClick` always wins.
datapointLabelChartDatapointLabelOverride the accessible name of each keyboard drill-down target (#349).
maxInteractiveDatapointsnumberDev-warning threshold on the number of keyboard targets. Default 500 (#349).
accessibleLabelChartA11yProps["accessibleLabel"]Accessible name for the chart region (announces to AT on focus).
accessibleDescriptionChartA11yProps["accessibleDescription"]Supplemental description read by AT (e.g. series names + value range).
paletteChartPaletteDefault fill for `Bar` children that don't set their own `fill`, via `resolvePalette` (RM-018) over the series count — so, for example, nine unfilled `Bar` series degrade to the neutral ladder with a dev warning instead of nine bars painted identically. A `Bar` with an explicit `fill` is never touched. Default: `"categorical"` (`resolvePalette`'s own default) — but see `applyBarPalette`: it only ever assigns colours to SERIES THAT WOULD OTHERWISE COLLIDE (2+ unfilled `Bar` children), so a chart with a single unfilled `Bar` keeps today's `--chart-line-primary`.
legendContainerLegendPropLegend engine (RM-118): `true` or a config object mounts `ChartLegend` beside the plot via `useContainerLegend`; `{ interactive: "toggle" }` turns each item into a real `aria-pressed` button that hides a series on grouped OR stacked bars — the value domain (`resolveBarValueDomain`, always zero-based) recomputes from the VISIBLE series only, and the hidden series' `<Bar>` never mounts, so it drops out of the datapoint layer too. `interactive: "hover"` (default) dims the other series via the existing `ChartLegendHoverProvider` seam `Bar` already reads. Unset renders NOTHING new (R1). Yields to `colorBy`'s own key — see its doc (R4, one key per chart). `{ values: true }` prints each series' total over every row (the `comparison` column's too; overlays show none), in the value axis' format (plain numbers for a percent stack, or when the series sit on value axes that format differently).
scrollbarChartCategoryNavigatorProps["scrollbar"]Overview strip style. Default `"none"`. `"miniChart"` / `"bar"` / `"auto"` mount it once the categories overflow `maxVisibleItems`.
annotationsreadonly ChartAnnotation[]Declarative annotations in data units: text notes, ranges, reference lines, row notes.
analyticsreadonly ChartAnalytic[]Statistical overlays computed from `data` (ADR 0040 §1): computed `line`/`band`s (average, median, percentile, std-dev, CI) drawn through the annotation layer, and `trend`/`window`/`forecast`/`errorBars` drawn as derived series with a legend entry, a tooltip row and an accessible sentence. Unset: no change.
tooltipbooleanShow a hover/focus tooltip. Default `true`: with no `<ChartTooltip>` child the chart adds a default one; a `<ChartTooltip>` child (for `variant`, `rows`, `content`, …) replaces it. `false` turns the default off.
selectionStatesChartSelectionStatesResolver<Record<string, unknown>>Maps a mark (category, optional series key, optional raw row) to its selection state. Unset → the chart renders exactly as before.
dimExcludedbooleanDim `excluded` marks and add their non-hue channel (the dashed frame). Default `true`; `false` keeps the `data-selection` attribute (so a host can style it) but paints nothing.
selectionGesturesreadonly ChartSelectionGesture[]Gestures to enable. Unset → no gesture layer, DOM byte-identical to today.
onSelectionIntentChartSelectionIntentHandler<Record<string, unknown>>Fires with every intent. Required for the gesture layer to mount.
selectionConfirmChartSelectionConfirmDefault `"immediate"`.
selectionFieldstringThe field name carried in intents. Default: the chart's `xDataKey`.
selectionHitRuleChartSelectionHitRuleRect / lasso hit rule. Default `"overlap"` (the report-builder BI suite).
selectionToolbar"auto" | "none"`"auto"` (default): a toolbar appears when gestures are enabled; `"none"` hides it.
windowNavigatorWindow | nullControlled window.
defaultWindowNavigatorWindowInitial window for the uncontrolled case.
onWindowChange(window: NavigatorWindow | null, meta: NavigatorChangeMeta) => void
minSpannumberSmallest window the user can make: a duration in ms (time) or a row count (index). Default: 5× the median step (time; the stock-chart library' rule) / 3 (index).
align"start" | "end"Where an automatic first window sits. `"end"` = the latest data (the associative BI suite `scrollStartPos: 1`). Default `"start"`.
maxVisibleItemsResponsive<number | "auto">Category families (RM-141): how many categories the plot shows at once (the associative BI suite "Number of bars"). Default `"auto"` — as many as keep a readable band (`maxReadableCategories`); with `scrollbar="auto"` the strip appears only once the categories overflow that count.
windowDomain"all" | "visible"Category families: `"all"` (default) keeps the value axis on the FULL data's domain while scrolling; `"visible"` refits it to the window.
zoombooleanPinch-to-zoom along the x axis (time x): two fingers on a touch screen, a trackpad pinch, or Ctrl/⌘ + wheel narrow the same window the strip moves; a two-finger drag pans it. `+` / `−` / `0` on the focused chart and the zoom buttons that appear once zoomed do the same from the keyboard. No strip is needed; with one, it follows. Default `true`; `false` leaves the gestures to the page. Off while the caller drives `xDomain` itself.

Also accepts: ChartSelectionProps, ChartSelectionGestureProps, FrameSizeGroupProps, Pick<LegendGroupProps, "legend">, Pick<ChartStateGroupProps, "status">, ChartCategoryNavigatorProps, Pick<TooltipGroupProps, "tooltip">

Bar

PropTypeDescription
dataKeyrequiredstringKey in data to use for y values
namestringSeries display name: legend and tooltip text. Unset falls back to `dataKey`, same as `Line`/`Area`'s `name`.
yAxisIdstring | numberY-scale group id for vertical bars (Recharts `yAxisId`). Default: `"left"`.
fillstringFill color for the bar. Can be a color, gradient url, or pattern url. Default: var(--chart-line-primary)
strokestringColor for tooltip dot. Use when fill is a gradient/pattern. Default: uses fill value
fillStyleSeriesFillStyleHow the bar is painted. `"solid"` (default) is the flat series fill — today's behaviour. `"hatch"` draws the series as an OUTLINED hairline hatch in its own colour, at any decoration level: the quiet voice next to a solid lead (a comparison, a projection, last year). `fill` must be a colour; a `url()` fill is left as authored. A per-row colour (`colorBy`, `highlightKey`) still wins and paints solid.
lineCapBarLineCapLine cap style for bar ends: "round", "butt", or a number for custom radius. Default: "round"
animatebooleanWhether to animate the bars. Default: true
animationTypeBarAnimationTypeAnimation type: "grow" (height) or "fade" (opacity + blur). Default: "grow"
fadedOpacitynumberOpacity when not hovered (when another bar is hovered). Default: 0.3
staggerDelaynumberStagger delay between bars in seconds. Auto-calculated if not provided.
stackGapnumberGap between this series' stacked segments and their neighbours, in pixels. Each boundary between two segments loses half of it from this side; the stack's baseline and total never move. Default: the parent `BarChart`'s `stackGap` (0).
groupGapnumberGap between grouped bars in pixels. Default: 4
showValuesBarShowValues@deprecated Use `labels` — the same values (ADR 0042 A.3, row 12). Read until 7.0.0, with one development warning; when both are set, `labels` wins.
labelsBarShowValuesPrint each bar's value as a `HaloText` label (the `text-chart-value` role, 800-weight — lieflat G3 Chunky Bars). `true`/`"outside"` place it just past the bar's far end; `"inside"` places it just inside. A negative bar's label is signed with a "−" (U+2212, not a hyphen) glyph. A bar narrower than `MIN_LABEL_BAR_WIDTH` hides its label rather than shrinking below `text-meta`. Default: off. A {@link BarShowValuesSpec} (RM-110) adds `"auto"` placement, hover-only visibility and an explicit `show` on/off. The object form's `placement` defaults to `"auto"` (inside when the label fits, else outside) — so `{ show: true }` is NOT the same as `true`, which always places the label outside. In a stack drawn from a layout (percent, diverging, ordered or totalled — RM-113) each segment centres its label whatever the placement, a percent segment prints its share, and a segment shorter than 24 px along the value axis stays unlabelled.
valueFormatChartValueFormatHow `labels` prints this series' numbers — a preset or a spec (`{ suffix: " %" }`, `{ style: "currency", abbreviate: true }`, `{ sign: "always" }`). One notation for the whole series, as without it. Unset: today's locale number. A percent stack still prints shares.
unitnumberDraw each bar as a countable `UnitStack` of `floor(value / unit)` rungs (vertical bars, `kind="rung"`) or ticks (horizontal bars, `kind="tick"`) instead of a solid fill — lieflat F1 Rung Bars. The rung pitch is derived from the value scale, not the bar's own pixel span, so one rung is worth the same amount in every column and the ladder never counts past the bar's own value (#241) — a value that is not an exact multiple of `unit` draws an honest partial. Width and opacity jitter via `seededRnd`, keyed per bar so neighbours never jitter in lockstep; every 5th unit draws heavier so the stack stays countable. Renders instantly (no `animate`/`animationType` grow-in). Default: off (solid fill).
highlightKeyBarHighlightKeyMark ONE bar as this series' "hero": it draws in `--chart-foreground` ink while every OTHER bar draws from `palette` instead of `fill` — lieflat's one-hero-bar-in-ink convention. A string/number is matched against the bar's category value; a function receives `(datum, index)`. Default: off (every bar draws `fill`).
paletteChartPalettePalette the non-hero bars draw from when `highlightKey` is set, via `resolvePalette` (RM-018). Default: `"categorical"`.
zeroLinebooleanDraw a 0.8px `--chart-foreground-muted` hairline at the zero baseline across the whole plot. Default: auto — shown whenever ANY bar series in this chart has a negative value; pass `false` to force it off on this series, or `true` to force it on even with all-positive data.

BarValueAxis

PropTypeDescription
position"top" | "bottom"Where the tick labels sit. Default: `"bottom"`.
tickCountAxisTickCountTick target (RM-192, ADR 0042 A.2, row 8). `"auto"` (default) is this axis' own default: 3 ticks below 320 px of plot width, 5 above.
numTicksnumber@deprecated Use `tickCount` — `numTicks` still wins when both are set, exactly as before. Read until 7.0.0, with one development warning.
valueFormatChartValueFormatHow the tick values print — a preset or a spec (`{ suffix: " h" }`). Default: `"compact"`.
titlestringA short name for the axis, printed after the last tick ("hours").

BarXAxis

PropTypeDescription
tickerHalfWidthnumberWidth of the date ticker box for fade calculation. Default: 50
showAllLabelsbooleanKeep every category (never stride over labels). Default: false. NOTE this no longer forces HORIZONTAL rendering: with dropping disabled the axis may still tilt, ellipsize, or hide itself when the labels cannot fit.
maxLabelsnumberMaximum number of labels to show. Default: 12
fitCategoryAxisFitHow labels that do not fit their band are rescued (RM-108): - `"auto"` (default) — horizontal → two-line `wrap` → 45° tilt → ellipsis → stride → hidden; - `"wrap"` — the same cascade without the tilt rung (never rotates); - `"tilt"` — the pre-RM-108 cascade (skips `wrap`); - `"off"` — pins the pre-fit behaviour: full labels, count-capped stride, no measurement and no reserved axis space. The regression escape hatch.
titleReactNodeAxis title — names what the categories are (RM-188; the same part `XAxis` draws).
titlePlacementAxisTitlePlacement`"outside"` (default): the title sits under the labels, flush with the plot's end edge. `"inside"`: hung inside the plot's bottom-end corner — the same fixed corner `XAxis` uses. By design it does not step around marks, so on a vertical bar chart it paints over the LAST bar (its halo keeps it legible); use `"outside"` when that bar must stay clear.

BarYAxis

PropTypeDescription
showAllLabelsbooleanKeep every category (never stride over labels). Default: true. NOTE this no longer implies every label renders in full: the axis still ellipsizes to the gutter it has, and hides itself when nothing legible fits.
maxLabelsnumberMaximum number of labels to show. Default: 20
fitCategoryAxisFit`"off"` pins the pre-fit behaviour — full labels, count-capped stride, no measurement and no reserved gutter. The regression escape hatch. Default: `"auto"`.
maxWidthnumberThe widest the label gutter may grow, in px. The gutter is measured from the labels and capped so a long name cannot eat the plot; the default cap suits one or two words. Raise it for rows whose names ARE the content — "Send Invoice → Payment Reminder" — and the chart still keeps its own minimum plot width. Default: 112.
titleReactNodeAxis title — names what the rows are (RM-188; the same part `YAxis` draws).
titlePlacementAxisTitlePlacement`"outside"` (default): the title sits above the top row label, in the label gutter. `"inside"`: hung inside the plot's top-start corner — the same fixed corner `YAxis` uses. By design it does not step around marks, so on a horizontal bar chart it paints over the FIRST bar (its halo keeps it legible); use `"outside"` when that bar must stay clear.

ChartTooltip

PropTypeDescription
showDatePillbooleanWhether to show the date pill under the plot. Default: shown when it fits the chart's bottom margin (≥ 36px) — never painted over a small plot.
showCrosshairbooleanWhether to show the vertical crosshair line. Default: true
showDotsbooleanWhether to show dots on the lines. Default: true
indicatorColorstring | ((point: Record<string, unknown>) => string)Color for the crosshair/indicator line. When a function, receives the hovered point (e.g. for candlestick: match candle color from close vs open). Default: --chart-crosshair.
content(props: { point: Record<string, unknown>; index: number }) => React.ReactNodeCustom content renderer for the tooltip box
rows(point: Record<string, unknown>) => TooltipRow[]Custom row renderer - return array of TooltipRow
variantChartTooltipVariantBox layout preset (RM-119). `"rows"` (default) is today's stacked `ChartTooltipContent`. `"table"` renders `ChartTooltipTable` — one column per series with a header row, the date in the `<caption>` (Datawrapper's dual-axis / multi-series shape, `dw-charts.md` §2.22). `"inline"` renders no box at all — `ChartTooltipInline` paints the hovered (or `focus`-nearest) series' value directly at the mark with `HaloText` (the bikes chart). Ignored when `content` is set — a custom renderer already owns the whole box.
focusbooleanDrive RM-112's per-series dim (`SeriesHoverDim`) from the tooltip's OWN nearest-series resolution: whichever series' y position is closest to the pointer, at ANY x along the crosshair, counts as hovered — not only a direct hover on that series' own 2px stroke. Works standalone: it registers "focus requested" on the shared series-mode context, so the dim fires with no `focusOnHover` prop on the `LineChart`/`AreaChart` container (a container `focusOnHover` still works exactly as before, and the two compose — either one turns the dim on). Also the default target series for `variant="inline"`. Default false.
pinbooleanTap-to-pin (RM-119): on a coarse pointer, a tap keeps the tooltip open instead of clearing the instant the finger lifts. A second tap on the mark, `Esc`, or a tap outside the chart releases it (announced once via `role="status" aria-live="polite"`). Default: `true` only when the pointer is coarse (`(pointer: coarse)` media query); pass an explicit boolean to force either way regardless of pointer type.
valueInTitlebooleanSwap the tooltip box's own title for nothing — the hovered value moves to the chart's OWN title instead (RM-119, #610). Inside a `ChartFrame` that shows a title this is automatic: while a row is hovered, pinned or keyboard-focused the frame's title reads "Apr: Revenue 40" and is announced once through the frame's polite status; leaving restores the title. No wiring. Outside a frame (or in `chrome="bare"`, a custom `headerSlot`, the expanded dialog) only the box title is dropped — the date/category is still on `useChart().tooltipData` for a custom title. Default false.
dotColorstring | ((point: Record<string, unknown>, line: LineConfig) => string)Override tooltip dot fill. When omitted and `rows` is set, dot colors match row colors. When a function, receives the hovered point and line config.
childrenReact.ReactNodeAdditional content to show below rows (e.g., markers)
classNamestringCustom class name
springConfigSpringConfigPer-chart override for the crosshair / dot / date-pill spring.
boxSpringConfigSpringConfigPer-chart override for the floating-panel spring.
panelStyleReact.CSSPropertiesInline styles for the tooltip panel (background, blur, etc.).
unitstringUnit text appended to every DEFAULT row's value (RM-109) — the same `unit` `<YAxis>` paints on one tick. Set automatically from the chart's own `<YAxis unit>` when unset (the shell threads it through); pass it explicitly only to override. Ignored when `rows` is set — a custom row builder owns its own `TooltipRow.unit` per row.
valueFormatChartValueFormatSTYLE to borrow from the chart's own `<YAxis valueFormat>` for DEFAULT rows (RM-109) — a currency symbol, a percent sign — never abbreviation (`ChartTooltipContent`'s doc). Set automatically from `<YAxis valueFormat>` when unset; ignored when `rows` is set.
currencystringISO 4217 code for `valueFormat`'s `style: "currency"`.

ChartLegend

PropTypeDescription
itemsrequiredLegendItem[]Legend items to display
hoveredIndexnumber | nullCurrently hovered index (for highlight effect)
onHover(index: number | null) => voidCallback when an item is hovered
onItemClick( item: LegendItem, index: number, event: React.MouseEvent | React.KeyboardEvent, ) => voidDrill-down on a legend entry (#349). When set, each item renders as a real `<button>` — keyboard operable and named by the item's label — instead of a `<div>`. Unset, the legend is unchanged (a non-interactive `<div>` list). NOTE this lives on `ChartLegend`, not on the chart containers: the legend is a separately-placed composition primitive, so a callback on the container would be a prop the container cannot honour.
hiddenKeysReadonlySet<string>Series toggle (RM-118, `legend={{ interactive: "toggle" }}`). Set together with `onToggleKey`: every item renders as a real `<button aria-pressed>` — native keyboard support, no extra wiring — and `hiddenKeys` marks which ones read `aria-pressed="false"` and paint dimmed with a struck-through label (WCAG 1.4.1: hidden is never colour-only). Independent of `onItemClick` (drill-down); a container sets one or the other.
onToggleKey(key: string, item: LegendItem, index: number) => voidToggles one item's hidden state. Keyed by `item.key ?? item.label`.
toggleControl"item" | "checkbox"What hides an item when `onToggleKey` is set. `"item"` (default): the whole entry is the `aria-pressed` toggle. `"checkbox"`: the entry stays free for `onItemClick` (a host's selection, say) and a small checkbox after it toggles visibility. The checkbox shows while the row is hovered or holds focus, and always on touch screens, which cannot hover. A hidden entry stays in the legend, muted and struck through, in both modes.
hideAtDensityreadonly ChartDensity[]Density tiers that hide the whole legend (RM-072 default: `xs`/`sm`). The container legend engine (`useContainerLegend`, RM-118) passes `["xs"]` — `sm` renders `stack`-laid-out instead of hiding — so this stays `["xs", "sm"]` for every OTHER (direct) caller, unchanged.
showProgressbooleanShow progress bars. Default: false
showMarkerbooleanShow color marker dot. Default: true
showValuebooleanShow numeric value column. Default: true
showPercentagebooleanShow percentage value. Default: true when showProgress is true
formatValue(value: number) => stringFormat function for displaying values. Overrides `valueFormat` entirely when set.
valueFormatChartValueFormatHow to format `value`/`maxValue` (RM-109) — a preset or the object form (`{ decimals, abbreviate, sign, prefix, suffix, … }`, `value-format.ts`). Resolved as a SET across every item (#250, `useChartValueSetFormatter`) so the legend never mixes "1K" beside "400". Default: locale-aware plain grouping (`intFmt`), unchanged from before this prop existed.
currencystringISO 4217 code for `valueFormat: "currency"`. Falls back to `ChartConfigProvider`, then `"USD"`.
maxFractionDigitsnumberMost digits after the decimal point in the value column (RM-187).
localestringLocale for the value and percent columns (RM-187); unset, the `LocaleProvider`'s.
titlestringTitle shown above the legend
layout"row" | "stack" | "split"Item flow direction (RM-118, sitting 3). `"stack"` (default — every caller before this prop existed) is byte-identical to before: one item per line. `"row"` wraps items left-to-right (`flex-wrap`), matching the layout `AutoChart`'s old `AutoLegend` used, AND drops the `w-full` an interactive (`onItemClick`/toggle) item otherwise gets — `w-full` inside a row would force every item onto its own line regardless of this prop. `useContainerLegend` (the container legend engine) passes its own resolved `layout` ("row" at `base`/`medium`/`wide`, "stack" at `narrow` or density `sm`) through; a direct caller may set its own or leave it unset for the original stacked behaviour.
classNamestringAdditional class name for the container
titleClassNamestringClass name for the title
itemClassNamestringClass name for each legend item
labelClassNamestringClass name for the label
valueClassNamestringClass name for the value
renderItem(props: { item: LegendItem; index: number; isHovered: boolean; isFaded: boolean; percentage: number; }) => ReactNodeCustom render function for legend items. `item.value` may be `NaN`, meaning the item has no value (a container legend entry with no number of its own): check `Number.isFinite(item.value)` before printing it. `percentage` is 0 for such an item.
splitGroupsreadonly ChartLegendSplitGroup[]`layout="split"` (RM-121): one row per value axis, each led by its side label. Items not named by any group are omitted. Unset, `"split"` renders as `"row"`.
splitStackedbooleanStack the split rows vertically (the narrow tier). Default `false`: side by side, wrapping.

Theming

The tokens each state resolves to. Change the token and every theme follows.

StateToken
series--chart-1 … --chart-5