1 GitHub star = 1 new component.

Star the repo
Charts & Statschartsvgzero-dependencyfunnelconversionmarketingdrop-off

Funnel Chart

Conversion funnel with tapered stages, step drop-off and overall conversion rate.

Live Preview

Signup funnel

Last 30 days

2.6%overall conversion
  1. Visited site48.2k
  2. Viewed pricing19.8k
  3. Started trial6.4k
  4. Activated3.9k
  5. Paid1.2k
View data
StageUsers% of topStep conversion
Visited site48.2k100.0%โ€”
Viewed pricing19.8k41.1%41.1%
Started trial6.4k13.3%32.3%
Activated3.9k8.1%60.9%
Paid1.2k2.6%31.8%

Usage

Copy and paste this code into your React component:

tsx
import { FunnelChart } from 'react-component-kit-ui/charts';

<FunnelChart stages={[
    { label: 'Visited', value: 48200 },
    { label: 'Trial', value: 6400 },
    { label: 'Paid', value: 1240 },
  ]} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/FunnelChart.tsx + FunnelChart.css:

FunnelChart.tsx
'use client';

import type { CSSProperties } from 'react';
import ChartFrame from './ChartFrame';
import { formatValue } from './chart-utils';
import './FunnelChart.css';

/** "a: b; --c: d" โ†’ style object */
function st(css: string | undefined): CSSProperties {
  const o: Record<string, string> = {};
  for (const decl of String(css ?? '').split(';')) {
    const i = decl.indexOf(':');
    if (i < 0) continue;
    const k = decl.slice(0, i).trim();
    if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
  }
  return o as CSSProperties;
}

/**
 * FunnelChart โ€” Conversion funnel with tapered stages, stage-to-stage drop-off
 * and overall conversion. Uses an ordinal single-hue ramp (stages are ordered).
 *
 * @param {Array<{label: string, value: number}>} stages - Ordered stages, widest first.
 * @param {string} color - Base hue for the ordinal ramp.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
export interface FunnelChartProps {
  title?: string;
  subtitle?: string;
  stages?: Array<{ label: string; value: number }>;
  color?: string;
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function FunnelChart({
  title = 'Signup funnel',
  subtitle = 'Last 30 days',
  stages = [
    { label: 'Visited site', value: 48200 },
    { label: 'Viewed pricing', value: 19800 },
    { label: 'Started trial', value: 6400 },
    { label: 'Activated', value: 3900 },
    { label: 'Paid', value: 1240 },
  ],
  color = 'var(--chart-1, #3987e5)',
  variant = 'glass',
  size = 'md',
  className = '',
}: FunnelChartProps) {
  const top = stages[0]?.value || 1;
  const overall = ((stages[stages.length - 1]?.value ?? 0) / top) * 100;
  const fmt = (v: number) => formatValue(v);
  return (
    <>
      <ChartFrame title={title} subtitle={subtitle} value={`${overall.toFixed(1)}%`} delta="overall conversion" variant={variant} size={size} className={className} table={{
          headers: ['Stage', 'Users', '% of top', 'Step conversion'],
          rows: stages.map((s, i) => [s.label, fmt(s.value), `${((s.value / top) * 100).toFixed(1)}%`, i ? `${((s.value / stages[i - 1].value) * 100).toFixed(1)}%` : 'โ€”']),
        }}>
        <ol className="fn" data-rk-9aefa4="">
          {stages.map((s, i) => {
            const pct = (s.value / top) * 100;
            const step = i ? (s.value / stages[i - 1].value) * 100 : 100;
            const shade = 100 - i * (55 / Math.max(1, stages.length - 1));
            return (
              <li className="fn__stage" data-rk-9aefa4="">
                <div className="fn__meta" data-rk-9aefa4="">
                  <span className="fn__label" data-rk-9aefa4="">{s.label}</span>
                  <span className="fn__value" data-rk-9aefa4="">{fmt(s.value)}</span>
                </div>
                <div className="fn__track" data-rk-9aefa4="">
                  <span className="fn__bar cf-mark" tabIndex={0} style={st(`width:${Math.max(2, pct)}%; background:color-mix(in srgb, ${color} ${shade}%, #0d0d0f)`)} aria-label={`${s.label}: ${fmt(s.value)}, ${pct.toFixed(1)}% of top${i ? `, ${step.toFixed(1)}% from previous stage` : ''}`} data-tip-title={s.label} data-tip={`${fmt(s.value)} users|${pct.toFixed(1)}% of top${i ? `|${step.toFixed(1)}% from previous` : ''}`} data-rk-9aefa4=""></span>
                </div>
                {i > 0 && (
                  <span className="fn__drop" aria-hidden="true" data-rk-9aefa4="">
                    <svg width="10" height="10" viewBox="0 0 10 10" data-rk-9aefa4=""><path d="M5 9 L9 3 H1 Z" fill="currentColor" data-rk-9aefa4="" /></svg>
                    {(100 - step).toFixed(0)}% drop
                  </span>
                )}
              </li>
            );
          })}
        </ol>
      </ChartFrame>
    </>
  );
}

export default FunnelChart;
FunnelChart.css
/* FunnelChart โ€” ported from astro-component-kit (charts/FunnelChart.astro) */

.fn:where([data-rk-9aefa4]) { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.85rem; }
.fn__stage:where([data-rk-9aefa4]) {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: 'meta drop' 'track track';
  gap: 0.35rem 1rem;
}
.fn__meta:where([data-rk-9aefa4]) { grid-area: meta; display: flex; gap: 0.75rem; align-items: baseline; }
.fn__label:where([data-rk-9aefa4]) { font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
.fn__value:where([data-rk-9aefa4]) { font-size: 0.875rem; font-weight: 650; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
.fn__drop:where([data-rk-9aefa4]) {
  grid-area: drop;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.75rem;
  color: var(--c-text-3, #8a8a8a);
}
.fn__drop:where([data-rk-9aefa4]) svg:where([data-rk-9aefa4]) { color: var(--chart-neg, #f06a6a); }
.fn__track:where([data-rk-9aefa4]) { grid-area: track; display: flex; justify-content: center; }
.fn__bar:where([data-rk-9aefa4]) {
  display: block;
  height: 30px;
  border-radius: 6px;
  transform-origin: center;
  animation: funnel-chart-fn-grow 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
}
.fn__bar:focus-visible:where([data-rk-9aefa4]) { outline: 2px solid var(--c-text-1, #fff); outline-offset: 2px; }
@keyframes funnel-chart-fn-grow { from { transform: scaleX(0.2); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .fn__bar:where([data-rk-9aefa4]) { animation: none; } }

Quick Info

Category
Charts & Stats
Filename
FunnelChart.tsx
Dependencies
None โ€” pure React + CSS
Tags
chartsvgzero-dependencyfunnelconversionmarketingdrop-off