Funnel Chart
- Stable
- WCAG 2.2 evidence
- RTL
A funnel chart: how many reach each stage of a process and what share is lost between stages.
Data driven: stages in, SVG out; the same props render the static SVG on the server. A plot of 420 px or more has three columns as
in Figma (names and counts, trapezoid bars on a centred axis, conversion pills); a narrower one stacks a label line above each
full-width bar so it reflows at 320 px. Accessibility: the figure is named by title and described by summary; the data table has every stage with its count, conversion,
drop-off and share of the first stage as text, so the drop-off is not only a narrowing bar (WCAG 1.1.1, 1.3.1, 1.4.1). Every stage
shows its name and count as text; the colour only orders the stages. The plot is ONE tab stop: Up and Down move between stages, Home
and End jump to the ends, Page Up and Page Down skip a tenth; each stage is announced with its count, conversion and drop-off. The
tooltip shows on hover and focus, stays while hovered and closes with Escape (1.4.13, 2.1.1). The draw-in is behind motion-safe:;
the plot stays left-to-right in right-to-left pages while the card mirrors, and digits follow the locale. Consumer duties: a title that asks the question, a summary with the main message, the unit of the counts, saying which scale the
widths use. The stage fills heatmap-2 and heatmap-3 are lighter than the 3:1 that a graphic needs on the card; they repeat what
the names and counts say, which is why this is acceptable.
import { FunnelChart } from "@nexera-ui/react";- 13
- examples
- 20
- props
- 4
- live controls
- 1
- platform
- 9
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart />Examples 12
The same examples as Storybook, rendered live. Open Code to copy one.
Drop off
pill="drop-off" shows the share lost at each step instead of the share kept. Both are always in the tooltip and the table.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart pill="drop-off" />;
Linear scale
scale="linear" draws the widths to scale: the late stages become slivers, at least 4 px wide.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart scale="linear" description="Q3 2026, all roles. Width is to scale." />;
Without pills
pill="none" leaves out the conversion column; the counts stay on the stage names.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart pill="none" />;
Missing values
A stage without a count draws no bar and reads "No data"; the next stage converts from the last one that has a count.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart
stages={[
{ name: "Applied", value: 1240 },
{ name: "Screened", value: null },
{ name: "Interviewed", value: 152 },
{ name: "Offered", value: 34 },
{ name: "Hired", value: 26 },
]}
/>;
Loading
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart state={{ status: "loading" }} />;
Empty
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart
stages={[]}
state={{
status: "empty",
title: "No applications in Q3",
description: "Try another quarter or clear the role filter.",
}}
/>;
Error state
import { Button, FunnelChart } from "@nexera-ui/react";
<FunnelChart
state={{
status: "error",
title: "Couldn't load the funnel",
description: "Check your connection and try again.",
action: (
<Button variant="secondary" size="sm">
Try again
</Button>
),
}}
/>;
Single point
One stage: a single full-width bar in the brand colour, no pills.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart
title="Applications this week"
description="One stage"
summary="40 applications this week."
stages={[{ name: "Applied", value: 40 }]}
kpi={{ value: "40", label: "applications" }}
/>;
Many points
Twelve stages: the plot grows with the stages (53 px each), the heat ramp repeats its steps and the keys reach every stage.
import { FunnelChart } from "@nexera-ui/react";
const MANY = Array.from({ length: 12 }, (_, i) => ({
name: `Stage ${String(i + 1)}`,
value: Math.round(5000 * Math.pow(0.78, i)),
}));
<FunnelChart
title="Onboarding checklist completion"
description="Twelve steps"
summary="Completion falls with every step, from 5,000 people at step one to 285 at step twelve."
stages={MANY}
kpi={{ value: "5.7%", label: "finish all twelve steps" }}
/>;
Long labels
Long stage names are shortened beside the bars (the tooltip, the point label and the table keep them in full); the title and header wrap.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart
title="Recruitment funnel from the first application to a signed contract across all departments and locations"
description="Q3 2026, all roles, all locations, including referrals, agencies and direct applications"
summary="Few applicants reach a signed contract."
stages={[
{ name: "Applications received through every channel", value: 1240 },
{ name: "Candidates screened by the recruiting team", value: 486 },
{ name: "Candidates interviewed by the hiring manager", value: 152 },
{ name: "Offers extended and under negotiation", value: 34 },
{ name: "Contracts signed and start date confirmed", value: 26 },
]}
/>;
Narrow
In a narrow container (under 420 px) the labels move above full-width bars, so the funnel reflows without cutting anything off.
import { FunnelChart } from "@nexera-ui/react";
export function Narrow() {
return (
<div className="max-w-72">
<FunnelChart />
</div>
);
}
Right to left
Right-to-left page: the card, header and table mirror, the plot keeps its left-to-right layout, digits and signs follow the locale.
import { FunnelChart } from "@nexera-ui/react";
<FunnelChart
title="قمع التوظيف"
description="الربع الثالث ٢٠٢٦، كل الوظائف"
summary="من ١٬٢٤٠ متقدمًا تم توظيف ٢٦، أي ٢٫١ بالمئة."
stages={[
{ name: "تقدّموا", value: 1240 },
{ name: "تمّ فرزهم", value: 486 },
{ name: "أُجريت معهم مقابلات", value: 152 },
{ name: "قُدّمت لهم عروض", value: 34 },
{ name: "تم توظيفهم", value: 26 },
]}
valueLabel="الأشخاص"
kpi={{ value: "٢٫١٪", label: "من التقدّم إلى التوظيف" }}
labels={{
step: "المرحلة",
conversion: "التحويل",
dropOff: "التسرّب",
overall: "من المرحلة الأولى",
}}
table={{ categoryLabel: "المرحلة", label: "عرض البيانات كجدول" }}
/>;
Props 20
Press "Try it" on a card to load that prop into the playground.
20 props shown
stages*Nexerareadonly FunnelStageInput[]The stages from the widest to the narrowest, top to bottom: how many reached each (applied, screened, interviewed, offered,
hired). A stage with value: null is a gap: no bar, "No data".
scaleNexera"sqrt" | "linear"How a count becomes a width. sqrt widens late stages that
a linear scale would draw as slivers; linear is to scale. Say which one you use in the description.
pillNexera"conversion" | "drop-off" | "none"What the pill beside each stage shows: the conversion from the stage before, the drop-off, or
none. The tooltip, the point label and the table always state conversion and drop-off in text.
valueLabelNexerastringName of what is counted ("People"): the tooltip row, the point label and the table column. Translate it.
heightNexeranumberHeight of the plot in px. Leave it out to size the plot to the number of stages: 280 px for five stages in the wide layout, a label line above every bar in the narrow one.
labelsNexeraPartial<FunnelLabels>Texts the chart invents: headers, and the names of conversion and drop-off. Translate them.
title*NexerastringChart title: the question the chart answers ("Headcount"). Required: it names the figure, the plot and the data table (WCAG 1.1.1).
descriptionNexeraReactNodeSecondary line under the title.
summary*NexerastringOne or two sentences with what the chart shows and its main message ("Headcount grew from 96 to 128 over twelve months"). Required: it describes the figure for assistive technology (WCAG 1.1.1); the data table has the details.
rangeNexeraChartRangePropsRange switch in the header: 3M, 6M, 12M. Pass the data of the chosen range back in.
actionsNexeraReactNodeExtra controls in the header, after the range switch.
kpiNexeraChartKpiPropsKPI row above the plot: the headline number with an optional label or change badge.
stateNexeraChartPlotStateThe state that replaces the plot: loading (skeleton), empty or error. A chart without a value to plot shows the empty state
by itself; pass { status: "empty", title, description, action } to give it your wording.
tableNexeraChartTableOptionsThe data table that is the text alternative: caption, first-column header and whether it is visible or only for assistive technology.
localeNexerastringLocale of digits and separators.
valueFormatNexeraNumberFormatOptionsIntl.NumberFormat options for values ({ style: "currency", currency: "PKR" }, { style: "percent" }). Serialisable, so it
works from a Server Component; formatValue needs a Client Component.
formatValueNexera(value: number) => stringFormats a value for tooltips, point labels and the table (full precision). Wins over valueFormat.
formatAxisValueNexera(value: number) => stringFormats an axis tick (short: "29M").
plotLabelNexerastringAccessible name of the plot group. Translate it.
noDataLabelNexerastringText for a missing value in the tooltip and the table. Translate it.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
8 direct · 1 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.10ReflowLevel AA · tested directly
- 1.4.13Content on Hover or FocusLevel AA · tested directly
- 2.1.1KeyboardLevel A · tested directly
- 2.3.3Animation from InteractionsLevel AAA · supporting test
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<FunnelChart className="shadow-sm" />Used in blocks
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.