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 category — across
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