Deck
Browse the catalog

Build / Molecules

Funnel

  • molecule
  • play-fn

Stages of a conversion, each measured twice: against the step before it, and against the top of the funnel.

Both readings are shown because they answer different questions and people reliably confuse them. From previous is the one you can act on — it says which step is leaking. Of total is the one that sets expectations — 3% of views becoming meetings sounds bad until you know 17% of saved contacts do. A funnel that reports only one of the two invites the wrong conclusion.

Percentages are derived here rather than passed in, so the arithmetic cannot drift from the counts printed beside it.

Bars are scaled to the first stage, which makes the narrowing itself the picture. Every figure is also written out, so nothing is locked in the bar.

import { Funnel } from '@meetcard/deck'

Example

<Funnel label="Conversion funnel" unit="views" stages={[ { label: 'Profile views', value: 1684, description: 'Everyone who opened your card' }, { label: 'Link clicks', value: 742, description: 'Tapped a link or CTA' }, ]} />

Stories

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Default
Source
const Default = () => <Card style={{ maxWidth: 520 }}>
    <Funnel
        label="Conversion funnel"
        unit="views"
        stages={[
          {
            label: 'Profile views',
            value: 1684,
            description: 'Everyone who opened your card',
          },
          { label: 'Link clicks', value: 742, description: 'Tapped a link or CTA' },
          { label: 'Contacts saved', value: 254, description: 'Kept your details' },
          {
            label: 'Meetings booked',
            value: 43,
            description: 'Book with Me + connected calendars',
          },
        ]} />
</Card>;

Two stages is still a funnel — the simplest question worth asking.

Two Stages
Source
const TwoStages = () => <Card style={{ maxWidth: 520 }}>
    <Funnel
        label="Save rate"
        unit="views"
        stages={[
          { label: 'Profile views', value: 320 },
          { label: 'Contacts saved', value: 118, description: 'Kept your details' },
        ]} />
</Card>;

A healthy funnel narrows gently. Worth having beside the default so the shape of a leak is recognisable by contrast.

Shallow Drop Off
Source
const ShallowDropOff = () => <Card style={{ maxWidth: 520 }}>
    <Funnel
        label="Conversion funnel"
        unit="views"
        stages={[
          { label: 'Profile views', value: 1684 },
          { label: 'Link clicks', value: 1502 },
          { label: 'Contacts saved', value: 1310 },
          { label: 'Meetings booked', value: 1104 },
        ]} />
</Card>;

A card that has been shared but not opened. Zero is a real answer, so the stages still render — with words instead of a division by zero.

Nothing Measured Yet
Source
const NothingMeasuredYet = () => <Card style={{ maxWidth: 520 }}>
    <Funnel
        label="Conversion funnel"
        unit="views"
        stages={[
          { label: 'Profile views', value: 0 },
          { label: 'Link clicks', value: 0 },
          { label: 'Contacts saved', value: 0 },
        ]} />
</Card>;

Props

PropTypeDefaultDescription
labelrequiredstring—Names the funnel for assistive tech, e.g. "Conversion funnel".
stagesrequiredFunnelStage[]——
formatValue(value: number) => string(value: number) => value.toLocaleString()—
unitstring'total'What the first stage counts, for the "% of views" reading — a noun, not a sentence. Defaults to "total".

Also accepts everything from Omit, so id, data-* and event handlers pass straight through.

Open in Storybookfor controls, the a11y panel and the interaction runner.