ChartWidget
The dashboard card a chart sits in — header, optional metric readout, and the body's selected state.
Usage
import { ChartWidget } from '@acronis-platform/ui-react';
<ChartWidget
header={{ title: 'Sessions', actions: <WidgetMenu /> }}
className="h-[300px] w-[592px]"
>
<AreaChart
config={config}
data={data}
dataKeys={['sessions']}
xKey="month"
className="size-full"
/>
</ChartWidget>;Every chart in the design is drawn inside the same card: a header with a title,
an optional filter chip and the ⋯ actions menu, then an optional metric readout,
then the plot. ChartWidget is that composition — and only that.
What it adds
One thing a Card legitimately doesn't know about:
what the body shows for a selected state. The consumer passes state
explicitly; ChartWidget does not infer state from the chart or its data. The
state replaces the metric and plot in the body.
Everything else is Card's.
The header is Card's
header is typed CardHeaderProps and spread straight onto CardHeader, so
the whole header surface works — including the parts this component never
mentions.
<ChartWidget
header={{
title: 'Active alerts by severity',
description: 'Grouped by severity',
hasDescription: true,
extras: <Tag variant="info">Last 6 months</Tag>,
actions: <WidgetMenu />,
isDraggable: true,
hasRename: true,
onRename: rename,
}}
>
{chart}
</ChartWidget>Omit header for a header-less widget — a dashboard tile whose heading lives
outside the card, or a chart embedded in a larger panel.
The per-type chart components stay card-less, so a chart is still usable
outside a widget: inside a table cell, a popover, or a
Metric's sparkline slot.
Metric readout
Pass a Metric to the metric slot to add a stats strip
to the widget. The orientation prop controls how metric and chart are arranged:
vertical(default) — metric above the chart, full width. The metric takes its natural height; the chart fills the rest of the card body. Best for md/lg widths (592px, 896px).horizontal— metric and chart side by side, eachflex-[1_0_0](equal halves). Best for sm width (288px) where stacking would leave too little height for the chart.
// vertical (default) — metric above the chart
<ChartWidget
header={{ title: 'Backup success rate' }}
metric={<Metric icon={<ChartPieIcon />} value="95" unit="%" />}
>
<AreaChart config={config} data={data} dataKeys={['rate']} xKey="month" className="size-full" />
</ChartWidget>
// horizontal — metric left, chart right
<ChartWidget
header={{ title: 'Backup success rate' }}
orientation="horizontal"
metric={
<Metric
icon={<ChartPieIcon />}
value="95"
unit="%"
trend="up"
trendValue="20%"
supportingText="over 6 months"
/>
}
>
<AreaChart config={config} data={data} dataKeys={['rate']} xKey="month" className="size-full" />
</ChartWidget>State
When the consumer passes state, ChartState
replaces the metric and plot while keeping the header. It fills the same body,
so the card keeps its size. The card chrome stays the same for all states.
<ChartWidget header={{ title: 'Severity' }} state="loading" />
<ChartWidget header={{ title: 'Sessions' }} state="empty" />
<ChartWidget header={{ title: 'Disk usage' }} state="number" stateDescription="0 GB" />
<ChartWidget
header={{ title: 'Sessions' }}
state="warning"
stateDescription="Some results could not be loaded"
/>
<ChartWidget
header={{ title: 'Sessions' }}
state="error"
stateDescription="We couldn't load these results"
/>stateDescription overrides the fallback content with localized or
context-specific text. For number, pass the numeric value and unit. The
consumer decides which state fits the widget's data and behavior.
It takes no size — the grid does
The Figma sets carry a size axis of sm / md / lg, but it only changes the
frame width — 288 / 592 / 896. The height is the dashboard's business.
So the widget declares no size; it passes one down. The card is a full-height
flex column, the header takes what it needs, and the body takes the rest — so a
chart given size-full fills the whole remaining card.
<div className="grid grid-cols-2 gap-4 [grid-auto-rows:300px]">
<ChartWidget header={{ title: 'Sessions' }}>
<AreaChart … className="size-full" />
</ChartWidget>
</div>Dropped into a parent with no definite height, the card hugs its content instead — so a standalone widget still works, with the chart bringing its own height.
bodyClassName covers the one remaining gap: a placeholder-only widget outside a
sized cell, where neither the grid nor the content can give a height.
<ChartWidget
header={{ title: 'Severity' }}
state="empty"
bodyClassName="h-[252px]"
/>Accessibility
The widget adds no role of its own — it's a grouping surface, and the meaning
lives in what it holds. To make each widget a landmark, use render, which is
forwarded to Card:
<ChartWidget
render={<section aria-label="Sessions" />}
header={{ title: 'Sessions' }}
>
{chart}
</ChartWidget>The accessible name has to come from you: the header's title is visible text
inside the region, not a name for it, so a landmark without an aria-label
(or an aria-labelledby pointing at the title) is an unnamed region.
The header's title is the widget's visible name, so a chart inside shouldn't
repeat it in its own accessible name. While state is set, ChartState is the
live region (role="status" for loading, empty, warning, and number, or
role="alert" for errors).
An icon-only control in actions needs its own aria-label — the ⋯ menu has no
visible text.