Funnel Chart
betaSteps that narrow, like joining or trials, as numbered rows with a pill bar, each step's share of the first, and how many stopped before it.
Preview
sign-ups
- Started a form: 64
- Paid: 51, 80% of the first step
- First login: 46, 72% of the first step
- Came back a second week: 46, 72% of the first step
own lines
- Started a trial: 40
- Came back a second day: 31, 78% of the first step
- Signed up: no figure
loading
Loading
empty
No one has started a form yet.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/funnel-chart
Use it for
- Steps people pass through in order, where some stop at each step: sign-up forms, trials, a checkout.
- Pass stages, first to last, each with a label and a value, and a title or aria-label.
- Each bar is its share of the first step. Each step after the first says how many stopped before it.
- Give a stage meta to replace the automatic 'stopped' line.
- Pass value: null for a step with no figure yet. It shows a dash, not a zero.
Avoid it for
- Parts of a whole or a ranking. Use BreakdownList. Its rows take the same shape.
- Steps that do not narrow, like a timeline. Use a list or a table.
- A trend over time. Use LineChart or AreaChart.
Props
- stages
Required. The steps, first to last. The same shape as a BreakdownList row, so one set of rows fits either.
{ id?: string; label: string; value: number | null; meta?: ReactNode }[]
- title
A visible title. It names the funnel. TypeScript requires title, aria-label, or aria-labelledby.
ReactNode
- aria-label
Names the funnel when it has no visible title.
string
- valueFormat
How each step's value prints, like v => `${v} people`. Options work from a server component. A function works from a client component only.
Intl.NumberFormatOptions | ((value: number) => string)
- locale
The locale for numbers.
string · default "en-US"
- loading
Shows placeholder rows, as many as stages, or three.
boolean · default false
- emptyText
Shown when stages is empty.
ReactNode · default "No data yet"
- className
Extra classes for the card. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| stages | { id?: string; label: string; value: number | null; meta?: ReactNode }[] | - | Required. The steps, first to last. The same shape as a BreakdownList row, so one set of rows fits either. |
| title | ReactNode | - | A visible title. It names the funnel. TypeScript requires title, aria-label, or aria-labelledby. |
| aria-label | string | - | Names the funnel when it has no visible title. |
| valueFormat | Intl.NumberFormatOptions | ((value: number) => string) | - | How each step's value prints, like v => `${v} people`. Options work from a server component. A function works from a client component only. |
| locale | string | "en-US" | The locale for numbers. |
| loading | boolean | false | Shows placeholder rows, as many as stages, or three. |
| emptyText | ReactNode | "No data yet" | Shown when stages is empty. |
| className | string | - | Extra classes for the card. Later classes win. |
Accessibility
- Renders a <figure> named by its title, or by aria-label. TypeScript requires one. The steps are an ordered list.
- Each step reads as its label, its value, and its share of the first step, then its meta, like 'Paid: 51, 80% of the first step, 13 stopped before this step'.
- A step with no value reads 'no figure', and shows a dash with no bar.
- The bars are hidden from screen readers. The track is quiet, so the numbers in text carry the meaning.
- Bars use --chart-1, which the theme holds to 3:1 on the page.
- Bars grow in once the funnel is on screen, over three times --motion-duration-base. Reduced motion and motion off draw them at once.
Examples
Each step, with its share of the first
<FunnelChart
title="New sign-ups this quarter"
stages={[
{ label: "Started a form", value: 64 },
{ label: "Paid", value: 51 },
{ label: "First login", value: 46 },
]}
/>With its own lines
<FunnelChart
aria-label="Free trials"
stages={[
{ label: "Started a trial", value: 40 },
{ label: "Came back a second day", value: 31, meta: "A holiday slowed 6 down" },
{ label: "Signed up", value: null, meta: "Counted on Friday" },
]}
/>