Skip to main content

ChartFrame

Containers and Specschart

Opt-in chart wrapper adding expand / flip-to-table / download-CSV to any chart child.

import { ChartFrame } 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 ChartFrame 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 ChartFrame up before writing code: `npx -y @elabs-ai/components-cli docs ChartFrame` 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.

Avoid

  • Expecting ChartFrame to read the chart's context — it is the PARENT of the chart's provider; pass the same data to both via props.
  • Importing @elabs-ai/components-data into a chart — charts → ui only; flip-to-table uses the @elabs-ai/components-ui Table.
  • Expecting the table/download controls with no `data` — they hide by design when data is absent or empty; pass the same rows you gave the chart.

Works with

Contains

Pairs with

Examples

20 live examples

Expanded

Expand modal opened via a play function interaction.

Table Flipped

Flip-to-table view active on load (via play interaction).

No Data

No data — download and table controls are hidden (degraded toolbar).

Features Subset

Only the expand control shown when features is restricted.

Loading

Loading vs ready (#268) — a layout-shaped skeleton body, toolbar suppressed.

With Source

The card contract's fourth part (lieflat) — an attribution/provenance footer, shown inline and (via the play function below) under the chart inside the expand modal too — never appended to the summary/detail pane (#184). A plain-string `source` also rides the downloaded CSV as a trailing `# source: …` comment row.

Download Callback

Download callback fires with the correct rows when triggered.

Export

Exports the chart's real rendered `<svg>` as SVG/PNG. `onExport` intercepts the generated file instead of triggering a real browser download, so the interaction test can inspect the `Blob` directly: the SVG is self-contained (every `var(--…)` colour resolved via `getComputedStyle` at export time, `<rect>` background painted from the card) and the PNG rasterises the same built SVG.

Density tiers

The same chart at the sheet-tile sizes a 24 × 12 fit grid produces at 1200 px. `density` only REMOVES furniture: `xs` drops every axis label, the legend, the description and the source row; `sm` keeps the category axis with at most four ticks; `md` is the default frame; `lg` is today’s frame at full width.

Keyboard Tooltip

Keyboard users see what hover shows (#447): tabbing onto a datapoint target shows the chart’s own tooltip, and tabbing away hides it again.

Value In Title

Value in the title (#610): `<ChartTooltip valueInTitle />` drops the box’s own title and the frame’s title reads the hovered row instead — “Apr: revenue 18,500”. Hover, tap-to-pin and keyboard focus all drive it, the change is announced once through the frame’s polite status, and leaving restores the title. Nothing to wire: the frame and the tooltip find each other.

Keyboard tooltip (Pie)

The same bridge on a family with its own hover vocabulary (#447): a Pie has no tooltip box — hovering a slice lifts it with a glow and names it in the donut centre — so focusing a slice’s keyboard target shows exactly that.

Tile Chrome

`chrome="tile"` for a dashboard sheet: the host tile owns the border and the header, so the frame draws no card. `headerSlot` and `menuSlot` replace the default title and toolbar; the menu receives the frame’s actions, so the expand modal still opens from the host’s own button and `onExpandChange` reports it.

Bare Chrome

`chrome="bare"`: the chart body only — for a host that draws everything else.

Interactions Off

`interactions` switches behaviour off without unmounting children (the nebula.js `Interactions` model): `passive: false` removes the tooltip, `select: false` keeps the keyboard layer but never fires `onDatapointClick`.

Overflowing Body

A fixed short body with genuinely tall content — the scrollable region ALWAYS overflows, so this deterministically (never intermittently) exercises the overflow-aware `tabIndex`/`role="group"`/`aria-label` fix (#432 round 3), and Storybook's own axe check (`a11y: { test: "error" }`) runs against a body that is guaranteed to be in the labelled/focusable state — proving `role="group"` (not `aria-label` on a bare `div`) satisfies axe's `aria-prohibited-attr` rule.

Editorial Chrome

Datawrapper’s chrome: a title that states the finding, a description whose colour chips replace the legend, italic notes, and the footer “Chart: Author • Source: Name (linked) • Get the data • Download image”. The SVG export carries the title, every painted tick label, the axis title, the legend and the source row; the PNG is 3× the frame’s CSS width.

Plain Export

`exportOptions={{ plain: true }}`: the picture without header or footer text.

Alt Text On Labelled Chart

A labelled chart (`accessibleLabel`) inside the frame takes the frame's `altText` as its description, ahead of the summary it would otherwise generate. Its own `accessibleDescription` would still win.

API

ChartFrame

PropTypeDescription
titleReactNodeWrite the title as the CONCLUSION, not the chart type — "Revenue is up 8% QoQ", not "Revenue chart". Put what each series means in prose in `description` (lieflat's card contract).
descriptionReactNodeProse that IS the legend — what a reader needs to read the chart correctly (series, units, scope), written as a sentence, not a caption. Bold the finding with `<strong>`; colour-key a series in the prose with `<InlineChip series="ram">short-term RAM</InlineChip>` — the chip takes the series' colour from the chart inside this frame, and its accessible name is the series name (RM-117).
notesReactNodeNotes under the chart (RM-117): method, definitions, caveats. Italic, above the footer; part of an exported picture.
bylineChartFrameByline"Chart: Author" at the start of the footer (RM-117). `kind` picks the word.
altTextstringText alternative for the whole picture (RM-117) — Datawrapper's "alt text". Becomes the chart figure's `aria-describedby` content when the chart has no description of its own; with no chart figure, the frame body becomes the figure it describes.
actionsChartFrameAction[]Footer links (RM-117), always drawn in Datawrapper's order: `"data"` (Get the data → CSV), your own nodes (an Embed link), `"svg"`, `"png"` (Download image). Built-ins degrade like the toolbar: no data → no `"data"`, no `<svg>` → no image links. Never part of an exported picture.
exportOptionsChartExportRequestDefaults for every export this frame starts — toolbar, menu and footer (RM-117).
footerLabelsPartial<ChartFooterLabels>Words of the footer row (RM-117) — pass your own to localise them.
dataRecord<string, unknown>[]Primary data input for table view and CSV download.
columnsChartFrameColumn[]Column definitions. Omitted → derived from Object.keys(data[0]). Determines column order/labels in both the table view and CSV export.
detailReactNodeRight-pane content in the expand modal. Defaults to a data summary (row count + per-numeric-column min/max/avg).
featuresChartFrameFeature[]Which toolbar controls to show. Defaults to all five. `table` and `download` are automatically hidden when `data` is absent/empty; `export-svg`/`export-png` are automatically hidden when the chart body has no `<svg>` (a non-chart placeholder, or the flipped-to-table view).
plotHeightResponsive<ChartPlotHeight>The chart's own drawing height (ADR 0039): px, or `{ aspect }` (width ÷ height), optionally per breakpoint. The title, legend, notes and source row are added AROUND it, so the frame is as tall as its content. Unset: the chart's family default (2 : 1, and 1.25 : 1 when narrow, for line/bar/…). With `chrome="tile"` and neither `plotHeight` nor `height`, the chart fills the tile's remaining height instead (#444). `plotHeight={260}` keeps the pre-ADR-0039 look of a card.
heightnumber@deprecated Use `plotHeight` — it sets the chart's own height, and the title, legend and notes are added around it. `height={n}` is now an alias for `plotHeight={n}` (it no longer fixes the body); removed in 7.0.0.
loadingbooleanLoading vs ready — renders a layout-shaped skeleton at the normal body height and suppresses the expand/flip-to-table/download toolbar (meaningless with no data yet). Default: `false`.
renderTable(rows: Record<string, unknown>[], columns: ChartFrameColumn[]) => ReactNodeCustom table renderer for the flip-to-table view. Defaults to the @elabs-ai/components-ui Table primitive.
onDownload(rows: Record<string, unknown>[], columns: ChartFrameColumn[]) => voidCustom CSV download handler. Defaults to the local RFC-4180 serializer (injection-guarded). The canonical reusable version is @elabs-ai/components-data `toCsv`; wire it here, or use the `chart-frame-data` registry block (`npx shadcn add chart-frame-data`) for the full sortable DataTable + downloadCsv.
onExportChartFrameExportHandlerRoutes an SVG/PNG export to the caller — with the generated `Blob` and filename — instead of triggering a local browser download. Mirrors `onDownload`, for apps that want to route an export through their own storage. When absent, the built-in handler downloads the file directly.
sourceReactNode | ChartFrameSourceLinkAttribution / provenance footer — e.g. "Source: Internal analytics, updated daily". Renders as the card's all-caps, letter-spaced source row (the fourth part of lieflat's card contract) inline and in the expand modal, under the chart in both — never appended to the summary statistics; when it is a plain string it also lands as a trailing `# source: …` comment row in the downloaded CSV, and as a bottom row in an SVG/PNG export. Hidden when absent. A string that overflows the row stays fully recoverable — a native `title` and, once it measurably overflows, a keyboard-reachable tooltip (#184). A non-string node keeps only the CSS caps with no overflow recovery — keep it short, or accept it may be visually truncated with no fallback. RM-117: `{ name, href }` names and links the source. With that form, a `byline` or `actions`, the source joins the footer row as "Source: Name" instead of the all-caps row.
chromeChartFrameChromeSurface drawn around the chart (RM-072). Default `"card"` — byte-identical to a frame without the prop. See `ChartFrameChrome`.
titleSize"default" | "headline"How loud the title is. `"default"` is a card title inside a dashboard — one of many. `"headline"` is a published figure standing on its own: the title takes the `text-title` role and balances its lines, and the description steps up to body size in the foreground ink, because in a standalone figure the description is reading text, not a caption.
headerSlotReactNode`chrome="tile"` only: replaces the default title/description header.
menuSlotReactNode | ((api: ChartFrameMenuApi) => ReactNode)`chrome="tile"` only: replaces the inline toolbar. A function receives the frame's actions, so a host kebab menu can open the expand modal, flip to the table or export without reaching into context.
onExpandChange(open: boolean) => voidFires when the expand modal opens or closes, however it was triggered.
interactionsChartInteractionsWhich interaction layers the chart mounts (`passive` tooltips, `active` brush/datapoint targets, `select` datapoint activation, `edit`). Defaults: all `true` except `edit: false`. Forwarded to every chart family through `useChartConfig()`.
selectionChartFrameSelectionOptionsSelection chrome defaults for the framed chart (RM-145). A chart listing `selectionGestures` puts its `ChartSelectionToolbar` in the frame's action row; `toolbar: "none"` keeps it out, and `confirm` sets the chart's default `selectionConfirm` (a brand theme modelled on the associative suite asks for `"explicit"`). The chart's own props always win.
densityResponsive<ChartDensity>Furniture tier (`"xs" | "sm" | "md" | "lg"`, default `"md"`). Forwarded to every chart family through `useChartConfig()`; the frame itself drops `description` and the source row at `xs` and clamps the title to one line at `xs`/`sm`. Optionally per breakpoint (ADR 0039): inside a `narrow` chart `md`/`lg` become `sm` unless an explicit `narrow` entry says otherwise (`{ base: "md", narrow: "md" }` keeps the legend and value axis).
childrenrequiredReactNodeThe chart content. Rendered in both inline and expanded modal positions.

Also accepts: Omit<HTMLAttributes<HTMLDivElement>, "title">

ChartCard

PropTypeDescription
titlerequiredReactNodeWrite the title as the CONCLUSION, not the chart type — "Revenue is up 8% QoQ", not "Revenue chart". Put what each series means in prose in `description` (lieflat's card contract).
titleAs"div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"Heading level for the card's title, forwarded to `CardTitle`'s own `as` prop (#328). Set this when the card titles a real page section so it contributes to the document outline. @default "div"
descriptionReactNodeProse that IS the legend — what a reader needs to read the chart correctly (series, units, scope), written as a sentence, not a caption.
actionsReactNodeHeader actions (range picker, menu).
childrenrequiredReactNodeThe chart itself. Bring your own chart lib (Recharts, visx, etc.).
heightnumberFixed body height so charts have a sizing context. Defaults to 260.
loadingbooleanLoading vs ready — the body becomes a layout-shaped skeleton at the same height; title/description keep rendering. Default: `false`.
sourceReactNodeAttribution / provenance footer — e.g. "Source: Internal analytics, updated daily". Renders as the card's all-caps, letter-spaced source row (the fourth part of lieflat's card contract); hidden when absent. A string that overflows the row stays fully recoverable — a native `title` and, once it measurably overflows, a keyboard-reachable tooltip (#184). A non-string node keeps only the CSS caps with no overflow recovery — keep it short, or accept it may be visually truncated with no fallback.

Also accepts: Omit<HTMLAttributes<HTMLDivElement>, "title">