Build / Molecules
Funnel
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
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.
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.
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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Names the funnel for assistive tech, e.g. "Conversion funnel". |
stagesrequired | FunnelStage[] | — | — |
formatValue | (value: number) => string | (value: number) => value.toLocaleString() | — |
unit | string | '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.