import { ChartFrame } from "@elabs-ai/components-charts";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-chartsPaste 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
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
| Prop | Type | Description |
|---|---|---|
title | ReactNode | Write 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). |
description | ReactNode | Prose 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). |
notes | ReactNode | Notes under the chart (RM-117): method, definitions, caveats. Italic, above the footer; part of an exported picture. |
byline | ChartFrameByline | "Chart: Author" at the start of the footer (RM-117). `kind` picks the word. |
altText | string | Text 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. |
actions | ChartFrameAction[] | 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. |
exportOptions | ChartExportRequest | Defaults for every export this frame starts — toolbar, menu and footer (RM-117). |
footerLabels | Partial<ChartFooterLabels> | Words of the footer row (RM-117) — pass your own to localise them. |
data | Record<string, unknown>[] | Primary data input for table view and CSV download. |
columns | ChartFrameColumn[] | Column definitions. Omitted → derived from Object.keys(data[0]). Determines column order/labels in both the table view and CSV export. |
detail | ReactNode | Right-pane content in the expand modal. Defaults to a data summary (row count + per-numeric-column min/max/avg). |
features | ChartFrameFeature[] | 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). |
plotHeight | Responsive<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. |
height | number | @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. |
loading | boolean | Loading 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[]) => ReactNode | Custom table renderer for the flip-to-table view. Defaults to the @elabs-ai/components-ui Table primitive. |
onDownload | (rows: Record<string, unknown>[], columns: ChartFrameColumn[]) => void | Custom 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. |
onExport | ChartFrameExportHandler | Routes 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. |
source | ReactNode | ChartFrameSourceLink | Attribution / 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. |
chrome | ChartFrameChrome | Surface 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. |
headerSlot | ReactNode | `chrome="tile"` only: replaces the default title/description header. |
menuSlot | ReactNode | ((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) => void | Fires when the expand modal opens or closes, however it was triggered. |
interactions | ChartInteractions | Which 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()`. |
selection | ChartFrameSelectionOptions | Selection 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. |
density | Responsive<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). |
childrenrequired | ReactNode | The chart content. Rendered in both inline and expanded modal positions. |
ChartCard
| Prop | Type | Description |
|---|---|---|
titlerequired | ReactNode | Write 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" |
description | ReactNode | Prose that IS the legend — what a reader needs to read the chart correctly (series, units, scope), written as a sentence, not a caption. |
actions | ReactNode | Header actions (range picker, menu). |
childrenrequired | ReactNode | The chart itself. Bring your own chart lib (Recharts, visx, etc.). |
height | number | Fixed body height so charts have a sizing context. Defaults to 260. |
loading | boolean | Loading vs ready — the body becomes a layout-shaped skeleton at the same height; title/description keep rendering. Default: `false`. |
source | ReactNode | Attribution / 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. |