Funnel Chart
Learn how to implement and customize Funnel Charts in your Nuxt application.
The FunnelChart component visualizes how a quantity narrows across sequential stages — perfect for conversion, sales-pipeline and drop-off analysis. New in v3.
Basic Usage
<script lang="ts" setup>
// FunnelChart takes a positional number[] aligned with `categories`.
// Largest first reads top-to-bottom.
const funnelData = [5000, 2300, 1200, 480];
const categories: Record<string, BulletLegendItemInterface> = {
visits: { name: "Visits", color: "var(--color-blue-400)" },
signups: { name: "Signups", color: "var(--color-pink-400)" },
trials: { name: "Trials", color: "var(--color-orange-400)" },
paid: { name: "Paid", color: "var(--color-green-400)" },
};
</script>
<template>
<FunnelChart
:data="funnelData"
:categories="categories"
:height="320"
variant="layered"
last-shape-type="rectangle"
:legend-position="LegendPosition.BottomCenter"
/>
</template>
Variants
Set variant="layered" for curved, nested layers with on-shape value / percentage / stage labels — a richer look for hero sections and dashboards. The default variant="default" renders the classic trapezoid funnel, and last-shape-type="rectangle" squares off the final stage.
<script lang="ts" setup>
const funnelData = [128400, 42800, 14600, 5200, 1480];
const categories: Record<string, BulletLegendItemInterface> = {
impressions: { name: "Impressions", color: "var(--color-violet-500)" },
clicks: { name: "Clicks", color: "var(--color-violet-400)" },
leads: { name: "Leads", color: "var(--color-sky-400)" },
demos: { name: "Demos", color: "var(--color-teal-400)" },
closed: { name: "Closed won", color: "var(--color-emerald-400)" },
};
const compact = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
</script>
<template>
<FunnelChart
:data="funnelData"
:categories="categories"
:height="300"
variant="layered"
last-shape-type="rectangle"
:value-formatter="(value: number) => compact.format(value)"
:hide-legend="true"
/>
</template>
Spacing and Value Formatting
stage-gap sets the vertical gap between stages in pixels (default 4). It applies to the classic variant="default" funnel; the layered variant spaces its own nested layers.
value-formatter controls the value rendered inside each shape and in the tooltip, in both variants — use it to abbreviate large numbers or add units.
<FunnelChart
:data="data"
:categories="categories"
:height="260"
:stage-gap="6"
last-shape-type="rectangle"
:value-formatter="(value) => value.toLocaleString()"
/>
Label Styling
Funnel labels split across two surfaces, and which one you reach for is decided by the label, not by taste:
| What | Where | Why |
|---|---|---|
| Size | label-size prop | The layered percentage badge is a real SVG pill sized around its own text. CSS cannot feed a computed font size back into that geometry, so a token here would desync the pill from its label. |
| Color | CSS token / theme.funnel | Pure paint. Belongs in the cascade so it inherits dark mode and your brand colors. |
| Weight | CSS token / theme.funnel | Same. |
Size
label-size defaults to 'auto', which fits the labels to the plot height and the per-stage width — one chart stays legible in a 220px card and in a full-width tile without you touching it. Pin it with a keyword, a pixel number, or per label role:
<!-- keyword: sm = 10px, md = 12px, lg = 14px -->
<FunnelChart
:data="data"
:categories="categories"
variant="layered"
:height="220"
label-size="sm"
/>
<!-- one pixel base; each role derives from it -->
<FunnelChart
:data="data"
:categories="categories"
variant="layered"
:height="220"
:label-size="11"
/>
<!-- per role -->
<FunnelChart
:data="data"
:categories="categories"
variant="layered"
:height="220"
:label-size="{ value: 13, percentage: 10, name: 11 }"
/>
A badge that still cannot fit its stage is dropped rather than allowed to overlap its neighbours.
Color and weight
Every label reads a --vc-funnel-* token. The defaults chain into the chart's own axis and tick tokens, which resolve against Nuxt UI's --ui-* layer first — so light/dark and a brand color change are inherited with no configuration at all.
| Token | Applies to | Default |
|---|---|---|
--vc-funnel-value-color | Stage value | --vc-axis-label-color |
--vc-funnel-value-weight | Stage value | 600 |
--vc-funnel-value-size | Stage value, default variant | --vc-legend-size |
--vc-funnel-label-color | Stage name | --vc-tick-color |
--vc-funnel-label-weight | Stage name | 500 |
--vc-funnel-badge-color | Percentage text | --vc-tick-color |
--vc-funnel-badge-weight | Percentage text | 600 |
--vc-funnel-badge-bg | Percentage pill fill | --vc-surface-bg |
--vc-funnel-badge-opacity | Percentage pill fill opacity | 0.82 |
--vc-funnel-badge-border | Percentage pill border | --vc-axis-line-color |
Set them globally in your CSS:
.vue-chrts {
--vc-funnel-value-color: var(--ui-primary);
--vc-funnel-value-weight: 700;
}
Or per chart, via the theme prop:
<FunnelChart
:data="data"
:categories="categories"
variant="layered"
:height="220"
:theme="{
funnel: {
valueColor: 'var(--ui-primary)',
valueWeight: 700,
labelColor: 'var(--ui-text-dimmed)',
badgeBg: 'var(--ui-bg-elevated)',
badgeOpacity: 1,
},
}"
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | number[] | Required | Stage values, in the same order as categories, or records with nameKey / valueKey. |
nameKey | keyof T | undefined | Record key holding the stage name, when data is an array of objects. |
valueKey | keyof T | undefined | Record key holding the stage value, when data is an array of objects. |
categories | Record<string, BulletLegendItemInterface> | Required | Maps each stage to its name and color. |
height | number | Required | Height of the chart in pixels. |
variant | 'default' | 'layered' | 'default' | Visual variant of the funnel. |
lastShapeType | 'triangle' | 'rectangle' | 'triangle' | Shape of the final (smallest) stage. |
showValueLabel | boolean | true | Show the value label on each stage. |
showStageLabel | boolean | true | Show the stage name inside each shape when it fits. |
stageGap | number | 4 | Vertical gap between classic funnel stages, in pixels. |
valueFormatter | (value: number, index: number) => string | undefined | Format values rendered inside shapes and in the tooltip. |
showPercentage | boolean | true | layered only. Show the percentage badge. |
labelSize | 'auto' | 'sm' | 'md' | 'lg' | number | FunnelLabelSizes | 'auto' | Size of the funnel's labels. See Label Styling. |
hideLegend | boolean | false | If true, hides the chart legend. |
legendPosition | LegendPosition | undefined | Position of the legend. |
legendStyle | Record<string, string> | undefined | Optional inline CSS styles for the legend container. |
hideTooltip | boolean | false | If true, hides the tooltip. |
tooltipTitleFormatter | (data: T) => string | number | undefined | Custom formatter for tooltip titles. |
tooltip | TooltipConfig | undefined | Tooltip configuration. |
theme | ChartTheme | undefined | Per-chart appearance overrides, incl. theme.funnel label color/weight. |
duration | number | undefined | Animation duration in milliseconds. |
Data Format
data is a positional array of numbers, ordered the same as categories. List the largest value first for a top-down funnel.
const data: number[] = [5000, 2300, 1200, 480];