Skip to content
txbrendan/ui

Funnel Chart

beta

Steps 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

New sign-ups this quarter
  1. Started a form: 64
  2. Paid: 51, 80% of the first step13 stopped before this step
  3. First login: 46, 72% of the first step5 stopped before this step
  4. Came back a second week: 46, 72% of the first stepNo one stopped before this step

own lines

Free trials
  1. Started a trial: 40From the launch post and the website
  2. Came back a second day: 31, 78% of the first stepA holiday slowed some down
  3. Signed up: no figureCounted on Friday

loading

New sign-ups this quarter

Loading

empty

New sign-ups this quarter

No one has started a form yet.

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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" },
  ]}
/>