Componentcharts0.93 KB gzipZero dependencies

Funnel Chart

Horizontal funnel stages showing conversion percentages between steps

Preview

Live Preview
Visitors-Leads20%Trials40%Customers37.5%

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add funnel-chart

Or install the full npm package:

npm install bigbullui
import { FunnelChart } from "@/components/ui/funnel-chart";

<FunnelChart stages={stages} />

Usage guidance

Use Funnel Chart when a ticket stub surface needs Horizontal funnel stages showing conversion percentages between steps Keep props minimal and prefer composition with sibling stubs.

Avoid Funnel Chart for long form flows or dense data grids where a dedicated layout block fits better.

Source code

funnel-chart.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface FunnelChartStage {
  label: string;
  value: number;
  conversion?: number;
  color?: string;
}

export interface FunnelChartProps extends React.HTMLAttributes<HTMLDivElement> {
  stages: FunnelChartStage[];
  height?: number;
}

export function FunnelChart({
  stages,
  height = 300,
  className,
  ...props
}: FunnelChartProps) {
  if (stages.length === 0) {
    return <div className={cn("w-full", className)} {...props} />;
  }

  const maxValue = Math.max(...stages.map((s) => s.value), 1);
  const n = stages.length;
  const topY = 6;
  const bandH = 88 / n;
  const fullW = 64;
  const cx = 54;

  return (
    <div
      className={cn("w-full", "motion-reduce:animate-none", className)}
      style={{ height: `${height}px` }}
    >
      <svg
        className="h-full w-full"
        viewBox="0 0 120 100"
        preserveAspectRatio="xMidYMid meet"
      >
        {stages.map((stage, stageIdx) => {
          const prevValue = stageIdx === 0 ? maxValue : stages[stageIdx - 1].value;
          const topW = Math.max((Math.max(prevValue, 0) / maxValue) * fullW, 2);
          const bottomW = Math.max((Math.max(stage.value, 0) / maxValue) * fullW, 2);
          const y0 = topY + stageIdx * bandH;
          const y1 = y0 + bandH - 2;
          const midY = (y0 + y1) / 2;
          const conversion = stage.conversion !== undefined ? `${stage.conversion}%` : "-";

          return (
            <g key={stage.label}>
              <polygon
                points={
                  `${cx - topW / 2},${y0} ` +
                  `${cx + topW / 2},${y0} ` +
                  `${cx + bottomW / 2},${y1} ` +
                  `${cx - bottomW / 2},${y1}`
                }
                fill={stage.color || "currentColor"}
                opacity={0.28 + (0.5 * (n - stageIdx)) / n}
              />
              <text
                x={cx - fullW / 2 - 3}
                y={midY + 3}
                fontSize="7"
                textAnchor="end"
                className="motion-reduce:transition-none fill-muted-foreground font-mono uppercase"
              >
                {stage.label}
              </text>
              <text
                x={cx + fullW / 2 + 3}
                y={midY + 3}
                fontSize="7"
                textAnchor="start"
                className="motion-reduce:transition-none fill-foreground font-mono"
              >
                {conversion}
              </text>
            </g>
          );
        })}
      </svg>
    </div>
  );
}

Props

PropTypeDescription
stagesFunnelChartStage[]
height?number