Funnel Chart
An SVG conversion funnel React component: trapezoid stages grow in one after another, stage-to-stage conversion labels sit between layers, hovering highlights a stage with a tooltip, in vertical and horizontal modes with a screen reader data table.
The chart behind every sign-up, checkout and trial-to-paid flow. Each stage is drawn as a symmetric trapezoid whose top width matches the bottom width of the stage above, with widths proportional to the values; very small stages keep at least 8% width so they never disappear. On entry the stages grow in one by one, 120ms apart, while their values fade in. Small labels between layers show the conversion relative to the previous stage, and hovering or keyboard-focusing a stage brightens it and shows a tooltip (stage name, value, share of the first stage); clicking selects it and fires onStageClick. When a label does not fit inside its trapezoid it moves to the side with a leader line.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add funnel-chart.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<FunnelChart/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/funnel-chart.en.jsonOr, once registries are configured in components.json, install it as @webberui/funnel-chart.
Usage
import { FunnelChart } from "@/components/ui/funnel-chart";
<FunnelChart
stages={[
{ id: "visit", label: "Visited home", value: 12480 },
{ id: "start", label: "Started sign-up", value: 6240 },
{ id: "form", label: "Filled details", value: 4180 },
{ id: "verify", label: "Verified email", value: 3050 },
{ id: "setup", label: "Finished setup", value: 2120 },
]}
orientation="vertical"
totalLabel="Overall conversion"
valueFormatter={(v) => `${v.toLocaleString()} users`}
onStageClick={(stage) => console.log(stage.label)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
stages | FunnelStage[] | — | Stage data, top to bottom (vertical) or left to right (horizontal); usually decreasing, but non-decreasing data is drawn as-is |
orientation | "vertical" | "horizontal" | "vertical" | Funnel direction |
showConversion | boolean | true | Show the conversion label (relative to the previous stage) between layers |
showValues | boolean | true | Show the value under each label |
valueFormatter | (value: number) => string | thousands-separated integer | Value formatting function |
totalLabel | string | — | When provided, shows the overall conversion (last ÷ first) above the chart with this string as its title |
height | number | 320 | Chart height (px); width is always 100% of the container |
gap | number | 6 | Spacing between layers (px) |
selectedId | string | null | — | Controlled selected stage id; when omitted the component manages it internally (uncontrolled) |
defaultSelectedId | string | null | null | Initial selected stage in uncontrolled mode |
onSelectedChange | (id: string | null) => void | — | Callback when the selection changes; clicking the selected stage again returns null |
onStageClick | (stage: FunnelStage, index: number) => void | — | Callback when a stage is clicked or activated from the keyboard |
ariaLabel | string | "轉換漏斗" | Accessible name of the chart, used for the summary and data table caption (default reads "conversion funnel") |
className | string | — | Forwarded to the outermost container |
FunnelStage
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique id, used for keys and selection |
label | string | — | Stage name |
value | number | — | Stage value; negatives are treated as 0 |
color | string | built-in color cycle | Override this stage's color (any valid CSS color) |
formatFunnelPercent(ratio) is also a named export: integers at 10% and above, one decimal below that, and "—" when the denominator is 0, so summary text outside the chart can reuse the same format.
How it works
- Trapezoid geometry: stage i's top width maps to
stages[i].valueand its bottom width to the next stage's value, so adjacent edges join seamlessly; the last stage has no next stage, so its bottom narrows to 82% of its top. - Color: by default a 6-color cycle from deep to light in the primary hue, overridable per stage with
stage.color; text inside each layer switches between dark and white based on the fill's brightness. - Conversion labels: drawn left of the layer boundary in vertical mode, and above it with a thin connector line in horizontal mode; increases are green with ↑, drops below half are red, everything else is neutral.
- Label placement: it first tries to place every label inside its trapezoid; only if some stage does not fit (too narrow or too short) does it reserve side space (right in vertical mode, bottom in horizontal mode) and move those labels out with a dashed leader line.
- Width follows the container: a ResizeObserver measures the real pixel width and recomputes the viewBox, so text keeps its font size instead of scaling with the container; switching orientation replays the grow-in animation.
Accessibility
- The SVG is
role="img"with anaria-labelsummary listing every stage's name, value and relative conversion, plusaria-describedbypointing at an sr-only data table; each layer also carries a<title> - Each layer is covered by a native
buttonclipped to the trapezoid with clip-path: Tab focuses it, arrow keys move between stages, Home / End jump to the ends, and Enter or Space selects;aria-pressedreflects the selection - Keyboard focus is shown with a 3px sky-blue outline on the layer, together with the same tooltip as hover
- When the user has reduce motion enabled at the system level, stages appear fully drawn without the grow and fade animations, and the tooltip switches instantly
Orbit Timeline
Events arranged around concentric orbits, expanding a detail card when one is selected.
Sankey Flow
Sankey diagram: automatic column layout, link widths proportional to value, animated gradient flows, and hover that traces a full path while dimming the rest — self-contained layout, no d3