Acronis UIKit
Components

ComposedChart

A typed composed chart — mix bars, lines, and areas on one axis.

Usage

import { ComposedChart } from '@acronis-platform/ui-react';
import type { ChartConfig } from '@acronis-platform/ui-react';

ComposedChart is a typed composition over the shared Chart primitives. Give it data, a per-series config, and a series list where each entry picks its own render type (bar / line / area) over one shared category axis (xKey) — it renders a themed recharts composed chart (tooltip, legend, axes, grid included), so you don't hand-compose recharts children.

Series colors come from the palette prop, not from config — see Palettes. Each config entry, keyed by a series[].key, maps that series to a label and an optional tone that re-points it within the palette. ComposedChart resolves them into the --color-<key> custom properties its marks paint from. Series render in the order you list them — later entries sit on top, whatever their mark type. Order them so a thin line comes after the bars/areas it should overlay.

Examples

The classic combo — bars for a quantity with a line for a related trend:

const data = [
  { month: 'Jan', revenue: 4200, forecast: 3800, profit: 2400 },
  { month: 'Feb', revenue: 3100, forecast: 2900, profit: 1400 },
  { month: 'Mar', revenue: 6500, forecast: 5200, profit: 4800 },
];

const config = {
  revenue: { label: 'Revenue' },
  forecast: {
    label: 'Forecast'
  },
  profit: {
    label: 'Profit'
  },
} satisfies ChartConfig;

<ComposedChart
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'profit', type: 'line' },
  ]}
  xKey="month"
  className="h-[320px] w-[560px]"
/>;

Mix all three render types — list them back-to-front (bar, then area, then line) so the line paints on top:

<ComposedChart
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'forecast', type: 'area' },
    { key: 'profit', type: 'line' },
  ]}
  xKey="month"
/>

Tune the shared knobs with curve (line/area interpolation), barRadius, fillOpacity, and the showGrid / showTooltip / showLegend toggles. legendPosition moves the legend above the plot, and tooltipCursor={false} drops the hover band behind the tooltip.

Add axis titles and a value-axis unit suffix with xAxisLabel / yAxisLabel / yUnit (the x-axis is categorical in the default orientation — see Orientation for the horizontal case, where xUnit takes over). The titles inherit the theme token:

<ComposedChart
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'profit', type: 'line' },
  ]}
  xKey="month"
  xAxisLabel="Month"
  yAxisLabel="Amount"
  yUnit="$"
/>

Orientation

orientation="horizontal" grows the marks rightward: the categories move to the y-axis, the values to the x-axis, the grid lines turn vertical, and each bar rounds its right end instead of its top. Reach for it when the category labels are long enough to crowd a bottom axis:

<ComposedChart
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'profit', type: 'line' },
  ]}
  xKey="month"
  orientation="horizontal"
/>

The value-axis props follow the values: xUnit and xTickFormatter format the tick values there, while yAxisTickCount / yAxisDomain keep their meaning ("whichever axis holds the values") and yTickFormatter formats the categories. A second value axis renders as a second x-axis along the top edge, which is also why yAxisOrientation goes inert here — the primary sits along the bottom and the secondary along the top, so neither has a left/right side to pick.

Per-series styling

Every styling prop on the chart is the default a series can override, keyed by the same name on its series[] entry — so one call sets the house style and each series departs from it only where it needs to:

<ComposedChart
  config={config}
  data={data}
  xKey="month"
  strokeWidth={3}
  series={[
    { key: 'revenue', type: 'bar', barSize: 18, barRadius: 2 },
    // A projection: dashed, thinner than the chart default, its own color.
    {
      key: 'forecast',
      type: 'line',
      strokeDasharray: '5 5',
      strokeWidth: 1.5,
      color: 'var(--ui-background-status-strong-warning)',
    },
    { key: 'profit', type: 'line', showDots: true },
  ]}
/>

The overridable set is color, curve, strokeWidth, strokeDasharray, showDots, showActiveDots, connectNulls, barRadius, barSize, showActiveBar, showBackground, fillOpacity, and legendType — plus stackId and yAxis, which have no chart-level counterpart. legendType: 'none' keeps a series off the legend while it still renders and appears in the tooltip; 'line' / 'rect' pick its marker (the wider recharts icon set isn't exposed — the legend draws its own marker and reads only those cases).

Bar geometry is set on the chart: barSize, barGap (between bars of one category), barCategoryGap (between the groups), and showBackground / backgroundFill for a track behind every bar. margin insets the plot — any side left out keeps the chart's own default, which is already widened when showLabels is on.

A null value breaks its line/area at that point; connectNulls bridges the gap, on the chart or on one series.

Stacking

Series that share a stackId are summed into one stack — bars with bars, areas with areas:

<ComposedChart
  config={config}
  data={data}
  xKey="month"
  series={[
    { key: 'revenue', type: 'bar', stackId: 'total' },
    { key: 'forecast', type: 'bar', stackId: 'total' },
    { key: 'profit', type: 'line' },
  ]}
/>

The ids are namespaced per mark type, so reusing one id across types can't merge a bar into an area's stack; a line ignores it (recharts doesn't stack lines). Only the segment at the top of a stack rounds its corners, and data labels centre in their own segment — a stacked segment has no free space at its growing end.

Reference lines and bands

referenceLine draws a dashed rule at a fixed value, at the average of one series (or of all of them with average: true), or — with categoryacross the categories at one of them: the hand-off between actuals and forecast. referenceArea shades a band behind a range of categories. Both accept an array:

<ComposedChart
  config={config}
  data={data}
  series={series}
  xKey="month"
  referenceLine={[
    { value: 6000, label: 'Target' },
    { average: 'revenue', label: 'Avg' },
    { category: 'Apr' },
  ]}
  referenceArea={{ from: 'Apr', label: 'Forecast' }}
/>

A band's from / to are inclusive and take either the category's own value or its row index; omit one to run to that end of the data. A reference whose category isn't in the data draws nothing rather than guessing a position.

A rule belongs to one scale, so on a chart with two value axes it is placed against the axis it was measured from: average naming a series reads off that series' own axis, average: true pools only the series on the axis the rule is drawn against, and yAxis overrides both — the way to put a fixed value on the secondary scale:

referenceLine={[
  // Read off the secondary axis, because `conversion` is measured against it.
  { average: 'conversion', label: 'Avg conversion' },
  { value: 5, yAxis: 'secondary', label: 'Target rate' },
]}

Without this, a rate's mean would be plotted against a count's scale and land flat on the baseline. A yAxis: 'secondary' request is ignored while no series has brought that axis into being.

Two value axes

Two measures whose units or magnitudes differ — a count and a rate — flatten each other on one scale: the smaller series sits on the baseline. A series opts in to a second axis with yAxis: 'secondary', and it renders on the side opposite the primary one:

<ComposedChart
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'conversion', type: 'line', yAxis: 'secondary' },
  ]}
  xKey="month"
  yTickFormatter={formatCompactNumber}
  secondaryYUnit="%"
/>

Each axis resolves independently: secondaryYAxisLabel, secondaryYUnit, secondaryYTickFormatter, secondaryYAxisTickCount, and secondaryYAxisDomain are the counterparts of the primary axis's yAxisLabel / yUnit / yTickFormatter / yAxisTickCount / yAxisDomain. Give the two the same domain preset when you want their tick rows to align — a tightly fitted (auto) secondary domain places its ticks between the primary axis's gridlines.

yAxisOrientation="right" moves the primary axis to the right; the secondary always takes the opposite side, so the pair mirrors as a whole. showSecondaryYAxis={false} keeps the second scale — the series stays measured against it — and drops only its ticks and title, the meaning showYAxis has for the primary axis.

The second axis exists only while a series asks for it, so the secondary props are inert on a single-scale chart, and the horizontal grid lines follow the primary axis (a second set from a different domain would cross the first at meaningless heights). If every series opts in, the primary axis has nothing measured against it: it isn't rendered, gives up its gutter, and the grid follows the secondary axis instead.

The two axes are told apart by position alone — neither the legend nor the tooltip names one, and the tooltip shows both values without their units. Label the axes (yAxisLabel / secondaryYAxisLabel) whenever the units aren't obvious from the series names.

Reach for one axis, not two, when a reader would compare the series value against value: with two scales, where the marks cross is a consequence of the domains you picked rather than a fact about the data.

Custom tooltip

Replace the tooltip with a configured ChartTooltipContent — imported from the same library, so you never compose recharts yourself. Its formatter renders each row and labelFormatter the header, giving you custom formatting, per-series content, and extra fields:

import { ComposedChart, ChartTooltipContent } from '@acronis-platform/ui-react';

<ComposedChart
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'profit', type: 'line' },
  ]}
  xKey="month"
  tooltipContent={
    <ChartTooltipContent
      labelFormatter={(label) => `${label} · fiscal Q3`}
      formatter={(value, name) => `${name}: $${value.toLocaleString()}`}
    />
  }
/>;

Format the tick values with xTickFormatter / yTickFormatter, or hide an axis with showXAxis / showYAxis — the shared axis knobs described under Formatting and hiding axes. Unlike unit, a formatter transforms the value (abbreviate, currency, map to a label):

import { ComposedChart, formatCompactNumber } from '@acronis-platform/ui-react';

<ComposedChart
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'profit', type: 'line' },
  ]}
  xKey="month"
  yTickFormatter={formatCompactNumber}
/>;

Animation

Charts render statically by default. Opt in to an entrance animation with animate, and tune it with animationDuration / animationBegin / animationEasing:

<ComposedChart
  config={config}
  data={data}
  series={series}
  xKey="month"
  animate
  animationDuration={800}
  animationEasing="ease-out"
/>

animate honors prefers-reduced-motion: for a visitor who has asked their system to reduce motion, the series render at their final geometry with no animation (the same applies when rendering on the server).

Data labels

Annotate each point with its value using showLabels. labelFormatter takes the same formatters as the axes, so a label and its axis read alike:

<ComposedChart
  config={config}
  data={data}
  series={series}
  xKey="month"
  showLabels
  labelFormatter={formatCompactNumber}
/>

labelPosition moves them; the default is the series' growing end (top). A label placed on an opaque bar fill switches to a token that stays legible over it; an area series keeps the on-surface color, since its translucent fill lets the surface show through:

<ComposedChart
  config={config}
  data={data}
  series={series}
  xKey="month"
  showLabels
  labelPosition="insideEnd"
/>

Labels are off by default — with more than one series they collide easily, so turn them on for a single series or a chart with room to spare.

Range brush

Long series don't have to be read all at once: showBrush renders a range selector under the plot. Drag a handle — or the selected window itself — and the series, its category axis, and the tooltip all follow the selection. brushHeight sizes the strip (28px by default):

<ComposedChart
  config={config}
  data={data}
  series={series}
  xKey="month"
  showBrush
/>

The brush indexes rows, so it slices the category axis. It is independent of showXAxis, so a chart with its axis hidden still gets a working brush. The two range captions appear on the strip while you hover, drag, or focus a handle.

Both handles are focusable role="slider" elements: Tab reaches them, and the arrow keys move the selection one row at a time. Pass brushAriaLabel to localize their accessible name (it defaults to 'Chart range selector').

Accessibility

Neither ComposedChart nor the shared ChartContainer adds a role or an aria-* attribute of its own. What semantics the chart has come from recharts, whose accessibility layer is on by default and is not switched off here: the plot's <svg> renders role="application" with tabindex="0", so Tab reaches it and focusing it opens the tooltip on the first category. The left/right arrow keys step it along the categories. Enter toggles the tooltip rather than pinning it, and since focus has already opened one, the first Enter closes it — press Enter again, or an arrow key, to bring it back.

That surface has no accessible name, though, and the layer to fix that is this one rather than recharts. recharts takes title, desc and role as first-class chart props and writes them straight into the plot's <svg>; ComposedChart passes none of them down, because it spreads the props it doesn't consume onto its own root <div> and hands the recharts chart only the data and margins it computes itself. The <title> and <desc> recharts always emits therefore stay empty and a screen reader announces an unnamed application region. The tooltip is not a live region either: ChartTooltipContent renders plain markup with no role="status" or aria-live, so the values it reveals as you arrow along are shown but never announced.

So name the chart from the outside: role and aria-label (or aria-labelledby) land on that same root <div>. Choose the role before you copy the snippet, though. role="img" names the chart and also makes its subtree presentational, hiding the plot from assistive technology while it stays in the tab order; that is the right trade when the picture is the whole message. Where the keyboard tooltip should stay reachable, wrap the chart in a <figure> with a <figcaption>, or pass role="group" with the same aria-label, and leave the plot's own semantics alone.

<ComposedChart
  role="img"
  aria-label="Monthly revenue with profit trend"
  config={config}
  data={data}
  series={[
    { key: 'revenue', type: 'bar' },
    { key: 'profit', type: 'line' },
  ]}
  xKey="month"
/>

A name is not a text alternative. The chart is a visual encoding of numbers, and "Revenue by month" says what the picture is about, not what it shows — so put the numbers in text as well: a caption carrying the takeaway, a summary sentence, or the same rows in an adjacent Table. Mixing mark types already gives each series a cue that isn't hue — a bar reads differently from a line whatever color it is — and strokeDasharray, showDots and showLabels add another where two series share a type. On a chart with two value axes neither the legend nor the tooltip says which scale a series belongs to, so label both axes when the units aren't obvious from the series names.

The range brush is the one part of the plot with real control semantics. Each traveller is a role="slider" element with tabindex="0", named by brushAriaLabel, and the left/right arrow keys move that end of the selection one row at a time — the traveller consumes those keys, so they resize the range instead of stepping the tooltip. ChartContainer restores the focus outline that its blanket outline-hidden would otherwise take away. Its aria-valuenow is recharts' own: the handle's pixel position, not a row index, so what a screen reader reads back from it carries no meaning.

Direction (RTL)

Under dir="rtl" the chrome around the plot mirrors on its own — the legend, the tooltip, and any readouts you compose alongside the chart are ordinary flow layout built from logical CSS utilities. The plot area itself deliberately stays left-to-right: recharts lays every mark out in physical SVG coordinates, so a mirrored category axis would flip the reading order of the data without flipping the geometry that encodes it. This is what mainstream charting libraries do by default, and it holds for orientation="horizontal" too: the marks keep growing rightward.

ChartContainer pins .recharts-surface to direction: ltr so that holds whatever the page inherits, and the pin is load-bearing rather than a formality: recharts anchors axis tick text with the direction-relative SVG keywords text-anchor: start / end while placing each mark at a coordinate it computed in physical space, so an inherited dir="rtl" mirrors the text away from the geometry — end-anchored Y-axis ticks land inside the plot area, rotated X-axis ticks drift, and a ReferenceLine label at the right edge overflows the surface. That last one matters here, since reference lines and bands are exactly that. Text anchored middle is direction-immune either way: unrotated X-axis ticks, axis titles, and the values showLabels draws. Because the pin stops at the surface, the tooltip and the legend — HTML outside it — still mirror. Chart carries the measured detail.

API Reference

Prop

Type

Edit on GitHub

On this page