Skip to content
skrewww

Containers & Overlays

Chart Card

Chart Card is a Card composed for a chart: an optional title/description/actions header, and a body that owns loading/empty/error presentation around a chart the consumer supplies as children.

Single component — state is a prop, not a variant

betaReact AvailableFigma UnavailableDocs Partial
React last updated
2026-09-22
Documentation last updated
2026-09-22
Accessibility target
WCAG 2.2 AA (target)
Version
0.1.0-beta

Known open questions

  • No Figma reference exists for Chart Card or its states — this implementation uses Card's own established visual language (spacing, type scale) conservatively; no Figma parity is claimed.
  • A canonical, generic time-range control (e.g. a shared TimeRangeTabs component) was evaluated and rejected for v1: Banking Balance Summary already proves the composition (Tabs in the `actions` slot, one TabsPanel per range, each holding its own chart instance) without a new abstraction. Compose with Tabs/ToggleGroup/Select/ButtonGroup directly; promote to a shared control only if a second, materially different real use case emerges.
  • Interactive legend (per-series toggling) remains out of scope, unchanged from CH-2 — Chart Card does not add one.
  • `errorAction`/`emptyDescription` accept arbitrary ReactNode but Chart Card fetches nothing itself — retry/action behavior is entirely the consumer's.
  • Chart Card was not retrofitted onto Banking Account Card or Banking Balance Summary — both predate it and remain unchanged; their duplicated metric-value styling (documented on Chart Metric) is left as recorded evidence, not migrated, since neither is trivial/zero-risk to change without its own review.

Purpose

A Card composed for a chart: an optional title/description/actions header, and a body that owns loading/empty/error presentation around a chart you supply as children.

Anatomy

A Card (no title passed to Card itself) whose body renders: an optional header row (title as a heading at the given `headingLevel`, an optional description, and an optional `actions` slot on the right, wrapping via flexbox — no media query), then a content region holding exactly one of: the `children` you supply (state="ready", the default), a Skeleton the size of `contentHeight` (state="loading"), an EmptyState with no icon or illustration (state="empty"), or an Alert (type="error", announced politely) (state="error"). Card's own `footer` passes straight through. No background, border, radius, or Shape/Surface property exists anywhere in Chart Card's own stylesheet — every surface property is Card's.

Variants and states

default

When to use

Wrapping a chart (Bar Chart, Line Chart, Area Chart) in a dashboard, with consistent loading/empty/error handling and an optional header row for a title, description, and actions (a time-range control, a filter, a menu).

When not to use

A plain content container with no chart — use Card directly. A page-blocking decision — use Dialog. An industry-specific widget (a named revenue/occupancy/vitals card) — compose Chart Card yourself; that naming and business meaning belongs to the consuming app, not this component.

Accessibility

The error state is announced (Alert, announce="polite"); loading exposes a visually-hidden label via aria-busy; the visual chart itself stays non-tabbable, unchanged from Bar/Line/Area Chart's own model.

Common mistakes

Inventing a `metric` prop instead of composing a Chart Metric into children; assuming `state="loading"`/`"empty"`/`"error"` still render children underneath (they don't — only one region renders); adding independent Shape/Surface props (Chart Card has none — it inherits Card's).

Properties

title, description, headingLevel (default h3), actions (header-right slot). elevation (passed through to Card). state ("ready" default | "loading" | "empty" | "error"). contentHeight (number, default 240 — a minimum, not a fixed height). loadingLabel, emptyTitle/emptyDescription, errorTitle/errorDescription/errorAction. children (rendered only when state is "ready"). footer (passed through to Card).

Why doesn't Chart Card have its own Shape/Surface props?

It renders inside Card's own body and adds no background, border, or radius anywhere in its stylesheet, so it inherits Card's Shape/Surface behavior automatically through the CSS cascade — the same mechanism Banking Account Card's own "Surface/Shape inheritance" note documents. Adding independent props here would just be a second way to set the same thing.

Why is there no `metric` prop?

Metric placement (before the chart, after it, beside it) is layout, not identity — it belongs in `children` alongside the chart, typically as a Chart Metric element, rather than as a dedicated slot with its own positioning rules to maintain.

Why does loading/empty/error hide `children` instead of layering on top?

A chart rendered underneath a loading skeleton or an error message would still mount with whatever `data` the consumer passed — usually stale or placeholder data — and "static chart, static children" is a much easier contract to reason about and test than a hidden-but-mounted chart.

Why is `contentHeight` a `minHeight`, not a fixed height?

A fixed height would either clip taller ready content or leave dead space under shorter content. A minimum keeps every state (including the Skeleton and EmptyState, which are told to fill it) from collapsing the card below a stable size, while still letting real content grow.

Tokens used

semantic/text/primarysemantic/text/secondarysemantic/surface/defaultsemantic/border/defaultcomponent/radius/container

Component API

PropTypeDefaultDescription
titlestringOptional header title.
descriptionReactNodeOptional header description, shown under the title.
headingLevel"h2" | "h3" | "h4""h3"Heading level for the optional title.
actionsReactNodeHeader-right slot — e.g. a time-range Tabs group, a filter Button, or a Menu of chart actions.
elevation"flat" | "raised"Passed through to the underlying Card.
state"ready" | "loading" | "empty" | "error""ready"Which region renders in the body.
contentHeightnumber240Minimum body height in pixels, matching a chart's own default height — keeps the card's size stable across every state.
loadingLabelstring"Loading chart"Visually-hidden label announced while state is "loading".
emptyTitlestring"No data"EmptyState title while state is "empty".
emptyDescriptionReactNodeEmptyState description while state is "empty".
errorTitlestring"Couldn't load chart"Alert title while state is "error".
errorDescriptionReactNodeAlert description while state is "error".
errorActionReactNodee.g. a "Retry" Button, rendered alongside the error message.
childrenReactNodeRendered only when state is "ready" — typically a Chart Metric and a chart.
footerReactNodePassed through to the underlying Card's footer.

React example

Copy React example
import { ChartCard, ChartMetric, LineChart } from "@/components/ui";

const trend = [
  { label: "Jan", value: 58 },
  { label: "Feb", value: 95 },
  { label: "Mar", value: 76 },
  { label: "Apr", value: 128 },
];

export function Example() {
  return (
    <ChartCard title="Monthly signups" description="Last 4 months">
      <ChartMetric label="Total" value="357" delta={{ direction: "up", value: "+12%", label: "vs prior period" }} />
      <LineChart data={trend} label="Monthly signups" showCategoryAxis tooltip />
    </ChartCard>
  );
}

export function LoadingExample() {
  return <ChartCard title="Monthly signups" state="loading" />;
}

export function EmptyExample() {
  return (
    <ChartCard title="Monthly signups" state="empty" emptyDescription="No signups recorded for this period." />
  );
}

export function ErrorExample() {
  return (
    <ChartCard title="Monthly signups" state="error" errorDescription="Something went wrong loading this chart." />
  );
}