Funnel Chart
Token-driven funnel chart for ordered pipeline stages, built on Recharts v3 — stacked or sideways, centred or from a shared edge, with the conversion rate from the first stage and from the previous one printed beside the shape, a staggered entrance and a named accessible figure.
pnpm add @tessinaui/uiWhen to use
A funnel answers one question: how many make it from one stage to the next, and where are they lost?
- Ordered stages of one process. Visitors, sign-ups, trials, paid. The order is the point: every stage is a subset of the one before it, or was meant to be.
- Three to about seven stages. Fewer than three is a single rate, which reads better as a number. Past seven the last stages are too thin to tell apart: group the early steps or split the funnel in two.
- When both rates matter. The share of the first stage says how much of the whole is left; the rate from the previous stage says where the loss happens. The chart prints both, each in its own place.
- Beside a headline number. A card whose title is the end-to-end rate and whose body is the funnel shows the result and its cause together.
Print the count and the rate, always. A funnel's shape is a picture of the
numbers, not a replacement for them: centred bars share no edge, so two
stages of similar size cannot be compared by eye. Where exact comparison
matters, grow the stages from a shared edge with align="start".
When not to use
- Categories with no order. Use BarChart.
- Change over time. A funnel is one period. To follow a conversion rate across weeks use LineChart.
- Parts of a whole that do not flow into each other. Use PieChart.
- One rate. Use Meter or a number.
- Paths that branch. A funnel has one way through. Steps people take in any order, or leave and rejoin, need a flow diagram.
- Stages as navigation. A row of stage tabs with counts is Tabs or Stepper, not a chart.
Three things to avoid when you do use one:
- Do not truncate a stage name. A stacked funnel gives every name a full line; a sideways one does not. With long names or a narrow column, keep the default orientation.
- Do not add a legend or a value axis. Every stage is named beside its bar and every value is printed; a key or a scale repeats them.
- Do not colour each stage differently. Stage order already carries identity. A second colour is a second meaning, and it has to be named.
Installation
pnpm add @tessinaui/ui rechartsUsage
import { FunnelChart } from "@tessinaui/ui";
const data = [
{ stage: "Visitors", people: 18240 },
{ stage: "Sign-ups", people: 4960 },
{ stage: "Trials", people: 1120 },
{ stage: "Paid", people: 386 },
];
<FunnelChart
data={data}
dataKey="people"
nameKey="stage"
label="One visitor in fifty becomes a paying customer"
/>Rows are the stages, in funnel order. config is optional: pass it to give a
stage a display label, or a colour of its own.
import type { ChartConfig } from "@tessinaui/ui";
// config is keyed by each stage's nameKey value
const config: ChartConfig = {
signups: { label: "Sign-ups" },
paid: { label: "Paid", color: "var(--chart-2)" },
};Examples
Default
Stages stack top to bottom, centred, in tones of one colour that run from the strongest at the top. The number beside each stage is its value and its share of the first stage; the number between two stages is the rate from one to the next. The stages draw in one after another, in funnel order.
Horizontal
orientation names the direction the funnel flows. "horizontal" runs the
stages along the inline axis, with each value over its stage and each name
under it. It needs room: on a phone, or with long stage names, keep the
default.
From a shared edge
align="start" grows every stage from the same edge, so lengths compare
exactly. showTrack draws a rail the length of the largest stage behind each
bar: what is left of the rail is what was lost.
Conversion rates
conversion chooses which rate prints: "first", "previous", "both"
(default) or "none". The share of the first stage always sits beside the
stage; the rate from the previous stage always sits between the two stages it
measures, so the two are never confused.
Formatting and labels
valueFormatter and percentFormatter feed the labels, the tooltip and the
summary assistive technology reads. With showLabels={false} the tooltip
carries every number.
Corner rounding
Connector
A wash of each stage's colour carries it into the next. curve="linear"
straightens its edges, showConnector={false} removes it, and stageGap and
maxStageSize set how much of each stage's pitch the bar takes.
Uneven data
A stage larger than the one before it is drawn longer and prints its real rate.
A stage at zero keeps its place and prints 0. A stage that is not measured is
null: it prints a dash, and no rate is computed through it.
A second colour
A color in config, or a stageColor callback, replaces a stage's tone.
Use it for a meaning the description names.
In a narrow card
In a column too narrow for the shape and both label columns, the share of the first stage moves to the tooltip and the value stays. A stage name that does not fit its column wraps onto a second line.
Right to left
dir="rtl" mirrors the layout: stage names move to the right, values and rates
to the left, and a sideways funnel runs right to left. An explicit dir also
switches the tooltip and summary strings to Arabic; strings replaces them for
any other language.
Accessibility
On a coloured plate
States
API Reference
FunnelChart
| Prop | Type | Default | Description |
|---|---|---|---|
data | FunnelChartDatum[] | — | One row per stage, in funnel order. |
dataKey | string | — | Datum key holding each stage's number. null or a missing value is "not tracked". |
nameKey | string | — | Datum key holding the stage name. |
config | ChartConfig | — | Optional per-stage label, icon, and color or theme, keyed by the stage's name. |
label | string | — | Accessible name; renders the chart as a named figure. State the insight. |
description | string | — | Visually hidden summary. A sentence per stage is always appended. |
orientation | "vertical" | "horizontal" | "vertical" | The direction the funnel flows. Vertical stacks stages top to bottom. |
align | "center" | "start" | "center" | Centre the stages, or grow them from a shared edge. |
size | "sm" | "md" | "lg" | "md" | Vertical: the pitch of one stage, 40 / 48 / 56px, so height follows the stage count. Horizontal: the aspect ratio, 3:1 / 2:1 / 16:9. |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Corner rounding of each stage. |
conversion | "both" | "first" | "previous" | "none" | "both" | Which conversion rates print. |
showLabels | boolean | true | Print each stage's value and rates. |
showCategoryAxis | boolean | true | Print the stage names. |
showTooltip | boolean | true | Render the tooltip. |
showConnector | boolean | true | Join each stage to the next with a wash of its colour. |
curve | "monotone" | "linear" | "monotone" | The connector's edge. |
showTrack | boolean | false | A rail the length of the largest stage behind each bar. With align="start". |
stageGap | number | string | "40%" | Space between two stages: px, or a percentage of one stage's pitch. |
maxStageSize | number | — | Cap on a stage's thickness, in px. |
valueFormatter | (value, name) => string | number | — | Formats values in labels, the tooltip and the summary. |
percentFormatter | (ratio, basis) => string | whole percents, one decimal under 10% | Formats rates. basis is "first" or "previous". |
stageColor | (datum, index, name) => string | undefined | — | Per-stage fill override. |
strings | Partial<FunnelChartStrings> | — | Replaces single entries of the built-in strings. |
animate | boolean | true | Draw the stages in one after another. Reduced motion turns it off regardless. |
dir | "ltr" | "rtl" | — | Direction; unset by default so a global dir is respected. |
loading | boolean | false | Render the skeleton. |
error | ReactNode | — | Render the error message instead of the chart. |
emptyState | ReactNode | "No data to display" | Shown with no rows, or when every stage is zero or untracked. |
children | ReactNode | — | Extra Recharts elements drawn inside the plot. |
Data attributes
| Attribute | On | Value |
|---|---|---|
data-component | root | "funnel-chart" |
data-slot | root | "chart" (from ChartContainer) |
data-orientation | root | "vertical" or "horizontal" |
data-align | root | "center" or "start" |
data-slot | connector group, connector | "funnel-chart-connectors", "funnel-chart-connector" |
data-slot | label group | "funnel-chart-labels" |
data-slot | value, share, rate text | "funnel-chart-value", "funnel-chart-share", "funnel-chart-step" |
Accessibility
labelis the accessible name. It renders the chart as a namedrole="figure". Without it the keyboard surface inside is an unnamed application. State the insight ("Half of all leads drop before a demo"), not the shape.- A static read needs no interaction. The chart writes a visually hidden
sentence per stage ("Leads: 6,800 (55% of Visitors)") and appends it to
description, through the same formatters the labels use. - The drawn labels are hidden from assistive technology, because that summary already reads every one of them.
- Arrow keys step through the stages. Down and Up in a stacked funnel,
the inline arrows in a sideways one, mirrored in a right-to-left layout.
Escapedismisses the tooltip without moving focus. - Nothing is hover-only. Pointing at a stage, tapping it and stepping to it with the keyboard mark it the same way and open the same tooltip.
- Colour is never the only encoding (WCAG 1.4.1). Position, length and the printed numbers carry every reading; the tones only reinforce order.
- Offer the numbers as a table wherever the funnel is the main content, as the accessibility example does.
- Every tone of the default ramp is at least 3:1 against the background in both themes; value and rate text is at least 4.5:1.
Screen-reader passes have not been recorded for this component yet.
Agent notes
- Rows are stages, in order. The chart does not sort: the first row is the top of the funnel and the base of every "of first" rate.
orientationnames the flow, not the bars."vertical"(default) is the stacked funnel, whose bars lie sideways.nullis "not tracked";0is a count of zero. They print and compute differently, so do not coerce a missing value to0.- A rate that cannot be computed is not printed: anything divided by a zero
or untracked stage. The chart never shows
NaNor∞. - Every stage at zero or untracked renders the empty state.
- A stacked funnel sets its own height from the stage count and
size. To size it yourself pass a height class inclassName. showTrackneedsalign="start"; a centred stage has no rail to fill.- The rate between two stages is hidden when the gap cannot hold it (a sideways funnel in a narrow column). It stays in the tooltip and the summary.
- Default number text is English-formatted (
12,400,55%) so the server and the browser render the same string. PassvalueFormatterandpercentFormatterto localise. configkeys are stage names, not series: one funnel is one measure.
Theming
A stage with no colour of its own takes a tone of --chart-1: the ramp steps
evenly across the band of tones that stay at least 3:1 against the background,
strongest at the top of the funnel in both themes. A color or theme in
config, or stageColor, replaces a stage's tone. The connector is a 25% wash
of the stage above it. Stage names use --muted-foreground; values and the
rate between stages use --foreground; the share of the first stage uses
--muted-foreground. On a coloured plate every stage takes --surface-ink in
one tone, and the texts take that ink and its muted tier. Corners follow
rounded on the library's radius scale.