Acronis UIKit
Components

FunnelChart

A typed funnel chart for stage-by-stage progression and drop-off.

Usage

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

FunnelChart is a typed composition over the shared Chart primitives. Give it data (one row per stage), a per-stage config, the value field (dataKey), and the label field (nameKey) — it renders a themed recharts funnel with a tooltip and a two-column legend beside it, so you don't hand-compose recharts children.

Nothing else is required. A funnel with only the data contract set draws four stages painted from the sequential blue ramp, no text on the plot (showLabels is false), the hover tooltip on (showTooltip), and the legend beside the funnel (showLegend is true). Stage labels stay off the plot because the legend already names every stage; turn on showLabels when you want them on the funnel as well.

Layout

The root is a flex row: a 120×120 square plot, a 16px gutter, then the legend column, which takes the width that's left. PieChart and RadialBarChart are composed the same way.

FunnelChart brings no width and no height of its own. It fills whatever its parent gives it: the plot stays a 120px square and the legend column absorbs the surplus, so a wider widget gets a wider legend rather than a taller wedge. Size the parent, not the chart — in a dashboard cell, pass className="size-full" and let the grid decide.

The legend sits on the inline end (the right in LTR), never below the plot.

FunnelChart is card-less. The card, its header, the ⋯ actions menu and the metric row all belong to ChartWidget, so the chart itself depends on no Card and reads just as well in a table cell or a popover:

<ChartWidget header={{ title: 'Conversion', actions: <WidgetMenu /> }}>
  <FunnelChart
    config={config}
    data={data}
    dataKey="value"
    nameKey="stage"
    className="size-full"
  />
</ChartWidget>

Examples

A conversion funnel with the default chrome — legend on the right, tooltip on hover, no text on the plot:

const data = [
  { stage: 'Visits', value: 5000 },
  { stage: 'Signups', value: 2600 },
  { stage: 'Trials', value: 1400 },
  { stage: 'Purchases', value: 620 },
];

const config = {
  Visits: { label: 'Visits' },
  Signups: { label: 'Signups' },
  Trials: { label: 'Trials' },
  Purchases: { label: 'Purchases' },
} satisfies ChartConfig;

<FunnelChart config={config} data={data} dataKey="value" nameKey="stage" />;

End the funnel flat instead of at a point with lastShape="rectangle":

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  lastShape="rectangle"
/>

Flip the direction with reversed so the funnel widens toward the bottom, or drop the tooltip with showTooltip={false}:

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  reversed
  showTooltip={false}
/>

Stage geometry

Each stage keeps 2px of surface between itself and the stage above it, and its corners are rounded by 2px. With the default lastShape="triangle" the final stage narrows to a point; lastShape="rectangle" ends it flat, as a stack of trapezoids. reversed stands the whole stack on its head, and the stage that ends up at the top of the plot is the one that takes no gap — the funnel's own outer edge is never a seam.

stroke / strokeWidth add a border to every stage, funnelWidth narrows the shape (px, or a percentage of the plot area), and showActiveShape outlines the hovered stage rather than changing its fill, so it keeps the colour that ties it to its legend entry.

strokeWidth on its own is enough: the renderer's own default stage border is a hardcoded white that the chart container neutralizes, so a bare strokeWidth would otherwise widen an invisible line. Given alone it pairs with --ui-border-on-surface-border.

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  stroke="var(--ui-border-on-surface-border)"
  strokeWidth={2}
  funnelWidth="65%"
  showActiveShape
/>

Coloring

palette is the only source of a stage's colour, apart from a per-stage stageSettings.color. config carries each stage's label and an optional tone, not its colour — see Palettes. FunnelChart resolves the palette into the --color-<name> custom properties its stages fill from, and hands the legend markers the same resolved colours.

The default is the sequential blue ramp, exported as FUNNEL_CHART_DEFAULT_PALETTE. Every other chart keeps the shared categorical default; a funnel doesn't, because its stages are an ordered series — one quantity dropping from stage to stage — and a ramp reads that order, where four unrelated categorical hues would not.

import { FUNNEL_CHART_DEFAULT_PALETTE } from '@acronis-platform/ui-react';

// The default, spelled out.
<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  palette={FUNNEL_CHART_DEFAULT_PALETTE}
/>;

// Another ramp, when blue is taken by a neighbouring widget.
<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  palette={{ type: 'sequential', ramp: 'teal' }}
/>;

stageSettings overrides one stage at a time, keyed by its nameKey value: color wins over the palette, and hidden drops the stage from the funnel, its labels, the legend, and the conversion percentages.

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  stageSettings={{
    Trials: { color: 'var(--ui-background-status-strong-info)' },
    Purchases: { hidden: true },
  }}
/>

Use the strong status tokens (--ui-background-status-strong-info, …) for a stageSettings colour, not the plain --ui-background-status-* ones — those are the light tints meant for banners and they wash out as a chart fill.

Legend

showLegend is on by default and renders one entry per visible stage as two columns: the coloured dot and the config label on the inline start, the stage's dataKey value on the inline end. Label and value both use the primary on-surface text token (--ui-text-on-surface-primary); the value is semibold. legendValueFormatter formats it:

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  legendValueFormatter={(value) => Number(value).toLocaleString()}
/>

Turn it off with showLegend={false} and the plot centres in the space it has — turn on showLabels too, or nothing names the stages any more.

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  showLegend={false}
  showLabels
/>

Labels

showLabels is off by default. When you turn it on, labelFormat decides what each stage's label says: its name (the default), its value, its percent, or a pair — name-value, name-percent, value-percent.

A percentage is the stage's share of the widest stage — its conversion from the top of the funnel. Unlike a pie's slices, a funnel's stages are nested subsets of each other rather than parts of a whole, so a share of the sum wouldn't mean anything. The base is the largest value, not the first row, so an unsorted funnel still tops out at 100.0%.

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  showLabels
  labelFormat="name-percent"
/>

labelPosition puts the label beside the stage (right, the default, or left) or on it (inside). The label colour follows: beside the funnel it sits on the card surface, on a stage it switches to the on-fill token so it keeps its contrast over a saturated stage colour. Override it with labelFill only when neither works on your surface.

inside only works while every stage is wide enough to hold its text. A funnel narrows by definition, so its last stages usually aren't — the text runs past the stage onto the surface, where the on-fill colour (white in both themes) has nothing to sit on and disappears in light mode. Pair inside with a short format (percent, value) and let the legend carry the names, or keep the labels beside the funnel.

showValueLabels adds a second label carrying the value. valuePosition places it, and defaults to the side opposite labelPosition — so it follows the names rather than pinning itself to one edge, and the two lists never stack:

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  showLabels
  showValueLabels
/>

labelFormatter formats only the numeric part of a label, so a label and its tooltip can share one formatter. percentFormatter formats the conversion share separately — it receives a fraction (0.52) and defaults to "52.0%":

import { createTickFormatter } from '@acronis-platform/ui-react';

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  showLabels
  labelFormat="name-percent"
  percentFormatter={createTickFormatter(
    { style: 'percent', minimumFractionDigits: 1 },
    'de-DE'
  )}
/>;

You don't normally need margin or funnelWidth: a composite labelFormat beside the funnel narrows the funnel on its own to leave the label room to wrap into.

Label room comes from funnelWidth, not margin. A label word-wraps against the gap between its own stage and the plot area's edge — and a margin moves that edge inward together with the funnel, so widening margin.right leaves a long label with less room, not more. Narrowing the funnel is what frees real space, because the plot area stays put. There is no equivalent lever for labelPosition="left": the widest stage always sits flush against the plot area's left edge, so a long left-hand label wraps regardless.

margin is merged over the defaults per side, so margin={{ right: 160 }} keeps the default top, bottom and left. Reach for it when a label can't wrap at all (one long word) and you need more room before the SVG edge.

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 { FunnelChart, ChartTooltipContent } from '@acronis-platform/ui-react';

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  tooltipContent={
    <ChartTooltipContent
      nameKey="stage"
      hideLabel
      formatter={(value, name) => `${name}: ${value.toLocaleString()}`}
    />
  }
/>;

Animation

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

<FunnelChart
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
  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).

Accessibility

FunnelChart sets no role and no aria-* of its own, and the SVG the funnel is drawn into has no accessible name. That gap belongs to this component rather than recharts: recharts takes title, desc and role as first-class chart props and writes them straight into the plot's <svg>, but FunnelChart passes none of them down — it spreads the props you give it onto its own root <div> and hands the recharts chart only the data it computes itself, so the <title> / <desc> pair recharts always emits stays empty. recharts' accessibility layer is on by default and is not switched off here, so the plot is also a tab stop carrying role="application": a focus stop that announces neither a name nor any data. The component exposes no way to turn that off.

So put the name on the root, which spreads the props you pass. Choose the role before copying the snippet: role="img" names the chart but also makes its subtree presentational, and it does not remove the focusable plot inside it — wrapping the chart in a <figure> with a <figcaption> is often the better fit here. aria-labelledby works the same way as aria-label.

<FunnelChart
  role="img"
  aria-label="Conversion funnel: 5,000 visits, 2,600 signups, 1,400 trials, 620 purchases"
  config={config}
  data={data}
  dataKey="value"
  nameKey="stage"
/>

Either way, a name is not a text alternative. A funnel is a visual encoding of numbers, so the numbers themselves have to be reachable as text — a caption, a summary sentence, or a table beside the chart. The default funnel puts nothing on the plot at all, so the legend carries the whole reading: it is ordinary HTML laid out beside the SVG, naming every stage and its value in the document. Turn on showLabels / showValueLabels and those are real <text> rather than pixels, but they sit in the SVG with no structural role, and a stage's name and its value end up as two separate label lists on opposite sides with nothing tying them together.

Direction (RTL)

Under dir="rtl" the chrome around the plot — the legend, the tooltip, and any readouts you compose alongside them — mirrors on its own, because it is ordinary flow layout built from logical CSS utilities. The plot area deliberately stays left-to-right: recharts places every mark in physical SVG coordinates, so mirroring it would flip the reading order of the data without flipping the geometry that encodes it. ChartContainer holds that in place by pinning .recharts-surface to direction: ltr.

For a funnel the pin is what fixes the labels, not just the stages. A funnel's labels are inside the plot, and recharts anchors a left- or right-positioned label with the direction-relative SVG keywords text-anchor: start / end — under an inherited dir="rtl" those would mirror each label back across the stage it names, while the stage itself stayed put. With the pin they don't, which is why labelPosition and valuePosition name physical sides: right is the right of the funnel in both directions, and the plot-area inset reserved for a label follows the same side. labelPosition="inside" is anchored middle and is direction-immune either way. The pin stops at the surface, so the legend and the tooltip — HTML outside it — still mirror with the page.

API Reference

Prop

Type

Edit on GitHub

On this page