Acronis UIKit
Components

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, each flex-[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.

Edit on GitHub

On this page