Funnel Chart — Conversion Funnel HTML CSS JS Snippet

Funnel Chart · Charts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Funnel from centred bars
Each bar is sized by a --w custom property and centred with margin: 0 auto, forming the tapering funnel with no SVG or clip-path.
Readable smallest stage
A min-width keeps even the narrowest stage wide enough to show its label and count, so the funnel base stays legible.
Conversion + drop-off rates
Every stage shows the step conversion above a red drop-off percentage, quantifying exactly where users are lost.
Staggered grow-in
An fn-grow keyframe animates each bar from zero to var(--w) with nth-child delays, so the funnel pours in top to bottom.
Variable-width keyframe
Animating to var(--w) lets one keyframe rule drive every stage regardless of its target width — no per-bar animation needed.
Click-to-focus toggle
focusStage rings the selected bar and dims the rest, then clears on a second click — built from two classes and one function.
Per-stage gradient
Each stage carries --c1/--c2 for its own gradient, giving the funnel a cohesive hue progression down the steps.
Data-light markup
Width, label, count, and rate are all editable inline, so swapping in real funnel data needs no JavaScript changes.

About this UI Snippet

Funnel Chart — Centred Tapering Bars, Stage Conversion Rates & Drop-Off Indicators

Screenshot of the Funnel Chart snippet rendered live

A funnel chart is the clearest way to show how users fall away across the steps of a process — visitors to sign-ups to activated to subscribed. Each stage is narrower than the last, and the shrinking width makes drop-off impossible to miss. This snippet implements a clean, animated funnel in plain HTML, CSS, and vanilla JavaScript: centred tapering bars sized by value, per-stage conversion and drop-off percentages, a staggered grow-in animation on load, and click-to-focus highlighting.

The funnel shape from centred bars

Each stage is a bar whose width is its value as a percentage of the top stage, set via a --w custom property and centred with margin: 0 auto. Because every bar is centred and each is narrower than the one above, the stack naturally forms the symmetric tapering funnel silhouette — no SVG polygons or clip-paths needed. A min-width guarantees even the smallest stage stays wide enough to hold its label and count, so the bottom of the funnel never becomes unreadable.

Two rates per stage: conversion and drop-off

Funnels are only useful if they quantify the fall-off. To the right of each bar, this snippet shows two figures: the step conversion rate (what fraction of the previous stage advanced) and the drop-off below it in red (the percentage lost). Seeing "47%" advance and "−53%" lost on the Subscribed row tells the story at a glance and points analysts straight at the leakiest step. A footer shows the overall end-to-end conversion.

Staggered grow-in animation

On load, each bar animates from width: 0 to its --w target with an fn-grow keyframe, and nth-child animation delays stagger the stages so the funnel "pours" in from top to bottom. The keyframe animates to var(--w), so the same rule works for every stage regardless of its target width. This entrance is what turns a static bar list into a chart that feels alive — and it draws the eye down the funnel in the order the steps actually happen.

Click-to-focus

focusStage adds interactivity: clicking a stage highlights it with a ring and dims the others, so you can isolate one step while presenting. Clicking the same stage again clears the focus. It is a toggle built from two classes — .active on the chosen bar and .dim on the rest — driven by one function, so it stays predictable.

The whole chart is data-light: change a stage's --w, label, count, and rate text to plug in real numbers, or render the stages from an array. Pair this funnel with a bar chart for category breakdowns, a metric card grid for headline KPIs, or a sparkline chart for trends.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA "Conversion funnel" card appears and five centred bars (Visitors → Renewed) grow in from top to bottom, forming a tapering funnel.
  2. 2
    Read the stagesEach bar shows its name and count; its width is proportional to the top stage, so the narrowing visualises drop-off.
  3. 3
    Check conversion and lossTo the right of each bar, the step conversion rate sits above the red drop-off percentage (e.g. 47% advanced, −53% lost).
  4. 4
    Click a stageClick any bar — it highlights with a ring and the others dim, letting you focus a single step. Click it again to clear.
  5. 5
    See overall conversionThe footer shows the end-to-end rate (7.25%), the fraction of visitors who reached the final stage.
  6. 6
    Plug in real dataEdit each stage's --w, label, count, and rate text — or render the stages from your analytics array.

Real-world uses

Common Use Cases

Marketing and sales funnels
Visualise visitor → lead → opportunity → customer drop-off. Sit it in a dashboard beside a metric card grid.
Product activation funnels
Track sign-up → activated → retained to find where onboarding leaks; pair with an onboarding tour.
E-commerce checkout funnels
Cart → shipping → payment → purchase conversion, highlighting the step that loses the most buyers before a checkout form.
Recruitment pipelines
Applicants → screened → interviewed → hired as a funnel, showing pass-through rates at each hiring stage.
Support and ticket flows
Opened → triaged → resolved → confirmed, surfacing where tickets stall in the process.
Analytics dashboards
Any multi-step conversion report; combine with a bar chart and sparkline chart for a full panel.

Got questions?

Frequently Asked Questions

Loop your stages array and build each .fn-stage, setting --w to value / stages[0].value * 100 + '%', the count text, and the rate. Compute the step conversion as value / prev.value and the drop-off as 1 − that. Keeping --w relative to the first stage is what produces the correct funnel proportions.

Centring each progressively-narrower bar creates the symmetric funnel silhouette that users instantly recognise as a conversion funnel. Left-aligned bars of decreasing width read as a plain bar chart instead. The margin: 0 auto centring is the single rule that turns bars into a funnel.

Add another line in the .fn-rate column with the lost count (prev.value − value), e.g. "−6,800 users". Many analysts want both the rate and the raw number, since a high percentage on a small stage can matter less than a small percentage on a huge one.

The bars are decorative, so expose the data textually: each stage already shows its name, count, and rate in real text. Add an aria-label per stage summarising it ("Sign-ups: 5,200, 43% of visitors"), and consider an off-screen data table for screen-reader users. Make focusable stages real buttons if click-to-focus is a primary interaction.

In React, map a stages array to bars, compute --w and rates with useMemo, and store the focused index in useState to drive active/dim classes. In Vue, use v-for with :style for --w and a ref for the focused stage. In Angular, *ngFor with [style.--w] and a focused index. The grow keyframe and centring CSS port unchanged.

Funnel Chart — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Funnel Chart</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .fn-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:440px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
    .fn-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:20px}
    .fn-title{font-size:17px;font-weight:800;color:#1e293b}
    .fn-sub{font-size:12px;color:#94a3b8;font-weight:600}
    
    .fn-chart{display:flex;flex-direction:column;gap:9px}
    .fn-stage{display:flex;align-items:center;gap:12px;cursor:pointer;transition:opacity .25s}
    .fn-stage.dim{opacity:.32}
    .fn-stage.active .fn-bar{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--c2),0 8px 18px rgba(99,102,241,.3)}
    
    .fn-bar{position:relative;height:48px;width:var(--w);min-width:120px;margin:0 auto;border-radius:11px;background:linear-gradient(90deg,var(--c1),var(--c2));display:flex;align-items:center;justify-content:space-between;padding:0 14px;color:#fff;overflow:hidden;animation:fn-grow .8s cubic-bezier(.4,0,.2,1) both;transition:box-shadow .2s}
    .fn-stage:nth-child(1) .fn-bar{animation-delay:.05s}
    .fn-stage:nth-child(2) .fn-bar{animation-delay:.15s}
    .fn-stage:nth-child(3) .fn-bar{animation-delay:.25s}
    .fn-stage:nth-child(4) .fn-bar{animation-delay:.35s}
    .fn-stage:nth-child(5) .fn-bar{animation-delay:.45s}
    @keyframes fn-grow{from{width:0;opacity:0}to{width:var(--w);opacity:1}}
    .fn-name{font-size:13px;font-weight:700;white-space:nowrap}
    .fn-count{font-size:13px;font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap;margin-left:10px}
    
    .fn-rate{width:74px;flex-shrink:0;text-align:right;display:flex;flex-direction:column;line-height:1.25}
    .fn-pct{font-size:14px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
    .fn-drop{font-size:10px;font-weight:700;color:#ef4444;min-height:12px}
    
    .fn-foot{margin-top:18px;padding-top:14px;border-top:1px solid #f1f5f9;font-size:12px;color:#64748b}
    .fn-foot strong{color:#6366f1;font-weight:800}
  </style>
</head>
<body>
  <div class="fn-card">
    <div class="fn-head">
      <h2 class="fn-title">Conversion funnel</h2>
      <span class="fn-sub">Last 30 days</span>
    </div>
  
    <div class="fn-chart" id="fnChart">
      <div class="fn-stage" style="--w:100%;--c1:#6366f1;--c2:#4f46e5" onclick="focusStage(this)">
        <div class="fn-bar"><span class="fn-name">Visitors</span><span class="fn-count">12,000</span></div>
        <div class="fn-rate"><span class="fn-pct">100%</span><span class="fn-drop">&nbsp;</span></div>
      </div>
      <div class="fn-stage" style="--w:43%;--c1:#7c83f4;--c2:#6366f1" onclick="focusStage(this)">
        <div class="fn-bar"><span class="fn-name">Sign-ups</span><span class="fn-count">5,200</span></div>
        <div class="fn-rate"><span class="fn-pct">43%</span><span class="fn-drop">−57%</span></div>
      </div>
      <div class="fn-stage" style="--w:26%;--c1:#8b5cf6;--c2:#7c3aed" onclick="focusStage(this)">
        <div class="fn-bar"><span class="fn-name">Activated</span><span class="fn-count">3,100</span></div>
        <div class="fn-rate"><span class="fn-pct">60%</span><span class="fn-drop">−40%</span></div>
      </div>
      <div class="fn-stage" style="--w:14%;--c1:#a855f7;--c2:#9333ea" onclick="focusStage(this)">
        <div class="fn-bar"><span class="fn-name">Subscribed</span><span class="fn-count">1,450</span></div>
        <div class="fn-rate"><span class="fn-pct">47%</span><span class="fn-drop">−53%</span></div>
      </div>
      <div class="fn-stage" style="--w:9%;--c1:#d946ef;--c2:#c026d3" onclick="focusStage(this)">
        <div class="fn-bar"><span class="fn-name">Renewed</span><span class="fn-count">870</span></div>
        <div class="fn-rate"><span class="fn-pct">60%</span><span class="fn-drop">−40%</span></div>
      </div>
    </div>
  
    <div class="fn-foot">Overall conversion <strong>7.25%</strong> · click a stage to focus</div>
  </div>
  <script>
    function focusStage(el) {
      var stages = el.parentElement.querySelectorAll('.fn-stage');
      var wasActive = el.classList.contains('active');
      stages.forEach(function (s) { s.classList.remove('active', 'dim'); });
      if (!wasActive) {
        el.classList.add('active');
        stages.forEach(function (s) { if (s !== el) s.classList.add('dim'); });
      }
    }
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Funnel Chart</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes fn-grow{from{width:0;opacity:0}to{width:var(--w);opacity:1}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
    }

    .fn-stage.dim {
      opacity:.32
    }

    .fn-stage.active .fn-bar {
      box-shadow:0 0 0 2px #fff,0 0 0 4px var(--c2),0 8px 18px rgba(99,102,241,.3)
    }

    .fn-stage:nth-child(1) .fn-bar {
      animation-delay:.05s
    }

    .fn-stage:nth-child(2) .fn-bar {
      animation-delay:.15s
    }

    .fn-stage:nth-child(3) .fn-bar {
      animation-delay:.25s
    }

    .fn-stage:nth-child(4) .fn-bar {
      animation-delay:.35s
    }

    .fn-stage:nth-child(5) .fn-bar {
      animation-delay:.45s
    }

    .fn-foot strong {
      color:#6366f1;font-weight:800
    }
  </style>
</head>
<body>
  <div class="bg-[#fff] border border-[#e2e8f0] rounded-[18px] p-6 w-full max-w-[440px] shadow-[0_14px_44px_rgba(15,23,42,.07)]">
    <div class="flex items-baseline justify-between mb-5">
      <h2 class="text-[17px] font-extrabold text-[#1e293b]">Conversion funnel</h2>
      <span class="text-xs text-[#94a3b8] font-semibold">Last 30 days</span>
    </div>
  
    <div class="flex flex-col gap-[9px]" id="fnChart">
      <div class="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style="--w:100%;--c1:#6366f1;--c2:#4f46e5" onclick="focusStage(this)">
        <div class="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span class="text-[13px] font-bold whitespace-nowrap">Visitors</span><span class="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">12,000</span></div>
        <div class="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span class="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">100%</span><span class="text-xs font-bold text-[#ef4444] min-h-[12px]">&nbsp;</span></div>
      </div>
      <div class="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style="--w:43%;--c1:#7c83f4;--c2:#6366f1" onclick="focusStage(this)">
        <div class="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span class="text-[13px] font-bold whitespace-nowrap">Sign-ups</span><span class="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">5,200</span></div>
        <div class="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span class="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">43%</span><span class="text-xs font-bold text-[#ef4444] min-h-[12px]">−57%</span></div>
      </div>
      <div class="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style="--w:26%;--c1:#8b5cf6;--c2:#7c3aed" onclick="focusStage(this)">
        <div class="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span class="text-[13px] font-bold whitespace-nowrap">Activated</span><span class="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">3,100</span></div>
        <div class="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span class="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">60%</span><span class="text-xs font-bold text-[#ef4444] min-h-[12px]">−40%</span></div>
      </div>
      <div class="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style="--w:14%;--c1:#a855f7;--c2:#9333ea" onclick="focusStage(this)">
        <div class="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span class="text-[13px] font-bold whitespace-nowrap">Subscribed</span><span class="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">1,450</span></div>
        <div class="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span class="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">47%</span><span class="text-xs font-bold text-[#ef4444] min-h-[12px]">−53%</span></div>
      </div>
      <div class="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style="--w:9%;--c1:#d946ef;--c2:#c026d3" onclick="focusStage(this)">
        <div class="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span class="text-[13px] font-bold whitespace-nowrap">Renewed</span><span class="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">870</span></div>
        <div class="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span class="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">60%</span><span class="text-xs font-bold text-[#ef4444] min-h-[12px]">−40%</span></div>
      </div>
    </div>
  
    <div class="fn-foot mt-[18px] pt-3.5 border-t border-t-[#f1f5f9] text-xs text-[#64748b]">Overall conversion <strong>7.25%</strong> · click a stage to focus</div>
  </div>
  <script>
    function focusStage(el) {
      var stages = el.parentElement.querySelectorAll('.fn-stage');
      var wasActive = el.classList.contains('active');
      stages.forEach(function (s) { s.classList.remove('active', 'dim'); });
      if (!wasActive) {
        el.classList.add('active');
        stages.forEach(function (s) { if (s !== el) s.classList.add('dim'); });
      }
    }
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to FunnelChart.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.fn-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:440px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.fn-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:20px}
.fn-title{font-size:17px;font-weight:800;color:#1e293b}
.fn-sub{font-size:12px;color:#94a3b8;font-weight:600}

.fn-chart{display:flex;flex-direction:column;gap:9px}
.fn-stage{display:flex;align-items:center;gap:12px;cursor:pointer;transition:opacity .25s}
.fn-stage.dim{opacity:.32}
.fn-stage.active .fn-bar{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--c2),0 8px 18px rgba(99,102,241,.3)}

.fn-bar{position:relative;height:48px;width:var(--w);min-width:120px;margin:0 auto;border-radius:11px;background:linear-gradient(90deg,var(--c1),var(--c2));display:flex;align-items:center;justify-content:space-between;padding:0 14px;color:#fff;overflow:hidden;animation:fn-grow .8s cubic-bezier(.4,0,.2,1) both;transition:box-shadow .2s}
.fn-stage:nth-child(1) .fn-bar{animation-delay:.05s}
.fn-stage:nth-child(2) .fn-bar{animation-delay:.15s}
.fn-stage:nth-child(3) .fn-bar{animation-delay:.25s}
.fn-stage:nth-child(4) .fn-bar{animation-delay:.35s}
.fn-stage:nth-child(5) .fn-bar{animation-delay:.45s}
@keyframes fn-grow{from{width:0;opacity:0}to{width:var(--w);opacity:1}}
.fn-name{font-size:13px;font-weight:700;white-space:nowrap}
.fn-count{font-size:13px;font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap;margin-left:10px}

.fn-rate{width:74px;flex-shrink:0;text-align:right;display:flex;flex-direction:column;line-height:1.25}
.fn-pct{font-size:14px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
.fn-drop{font-size:10px;font-weight:700;color:#ef4444;min-height:12px}

.fn-foot{margin-top:18px;padding-top:14px;border-top:1px solid #f1f5f9;font-size:12px;color:#64748b}
.fn-foot strong{color:#6366f1;font-weight:800}
`;

export default function FunnelChart() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    function focusStage(el) {
      var stages = el.parentElement.querySelectorAll('.fn-stage');
      var wasActive = el.classList.contains('active');
      stages.forEach(function (s) { s.classList.remove('active', 'dim'); });
      if (!wasActive) {
        el.classList.add('active');
        stages.forEach(function (s) { if (s !== el) s.classList.add('dim'); });
      }
    }
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof focusStage === 'function') window.focusStage = focusStage;
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="fn-card">
        <div className="fn-head">
          <h2 className="fn-title">Conversion funnel</h2>
          <span className="fn-sub">Last 30 days</span>
        </div>
      
        <div className="fn-chart" id="fnChart">
          <div className="fn-stage" style={{ '--w': '100%', '--c1': '#6366f1', '--c2': '#4f46e5' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar"><span className="fn-name">Visitors</span><span className="fn-count">12,000</span></div>
            <div className="fn-rate"><span className="fn-pct">100%</span><span className="fn-drop">&nbsp;</span></div>
          </div>
          <div className="fn-stage" style={{ '--w': '43%', '--c1': '#7c83f4', '--c2': '#6366f1' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar"><span className="fn-name">Sign-ups</span><span className="fn-count">5,200</span></div>
            <div className="fn-rate"><span className="fn-pct">43%</span><span className="fn-drop">−57%</span></div>
          </div>
          <div className="fn-stage" style={{ '--w': '26%', '--c1': '#8b5cf6', '--c2': '#7c3aed' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar"><span className="fn-name">Activated</span><span className="fn-count">3,100</span></div>
            <div className="fn-rate"><span className="fn-pct">60%</span><span className="fn-drop">−40%</span></div>
          </div>
          <div className="fn-stage" style={{ '--w': '14%', '--c1': '#a855f7', '--c2': '#9333ea' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar"><span className="fn-name">Subscribed</span><span className="fn-count">1,450</span></div>
            <div className="fn-rate"><span className="fn-pct">47%</span><span className="fn-drop">−53%</span></div>
          </div>
          <div className="fn-stage" style={{ '--w': '9%', '--c1': '#d946ef', '--c2': '#c026d3' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar"><span className="fn-name">Renewed</span><span className="fn-count">870</span></div>
            <div className="fn-rate"><span className="fn-pct">60%</span><span className="fn-drop">−40%</span></div>
          </div>
        </div>
      
        <div className="fn-foot">Overall conversion <strong>7.25%</strong> · click a stage to focus</div>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function FunnelChart() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    function focusStage(el) {
      var stages = el.parentElement.querySelectorAll('.fn-stage');
      var wasActive = el.classList.contains('active');
      stages.forEach(function (s) { s.classList.remove('active', 'dim'); });
      if (!wasActive) {
        el.classList.add('active');
        stages.forEach(function (s) { if (s !== el) s.classList.add('dim'); });
      }
    }
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof focusStage === 'function') window.focusStage = focusStage;
  }, []);

  return (
    <>
      <style>{`
@keyframes fn-grow{from{width:0;opacity:0}to{width:var(--w);opacity:1}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}

.fn-stage.dim {
  opacity:.32
}

.fn-stage.active .fn-bar {
  box-shadow:0 0 0 2px #fff,0 0 0 4px var(--c2),0 8px 18px rgba(99,102,241,.3)
}

.fn-stage:nth-child(1) .fn-bar {
  animation-delay:.05s
}

.fn-stage:nth-child(2) .fn-bar {
  animation-delay:.15s
}

.fn-stage:nth-child(3) .fn-bar {
  animation-delay:.25s
}

.fn-stage:nth-child(4) .fn-bar {
  animation-delay:.35s
}

.fn-stage:nth-child(5) .fn-bar {
  animation-delay:.45s
}

.fn-foot strong {
  color:#6366f1;font-weight:800
}
      `}</style>
      <div className="bg-[#fff] border border-[#e2e8f0] rounded-[18px] p-6 w-full max-w-[440px] shadow-[0_14px_44px_rgba(15,23,42,.07)]">
        <div className="flex items-baseline justify-between mb-5">
          <h2 className="text-[17px] font-extrabold text-[#1e293b]">Conversion funnel</h2>
          <span className="text-xs text-[#94a3b8] font-semibold">Last 30 days</span>
        </div>
      
        <div className="flex flex-col gap-[9px]" id="fnChart">
          <div className="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style={{ '--w': '100%', '--c1': '#6366f1', '--c2': '#4f46e5' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span className="text-[13px] font-bold whitespace-nowrap">Visitors</span><span className="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">12,000</span></div>
            <div className="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span className="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">100%</span><span className="text-xs font-bold text-[#ef4444] min-h-[12px]">&nbsp;</span></div>
          </div>
          <div className="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style={{ '--w': '43%', '--c1': '#7c83f4', '--c2': '#6366f1' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span className="text-[13px] font-bold whitespace-nowrap">Sign-ups</span><span className="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">5,200</span></div>
            <div className="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span className="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">43%</span><span className="text-xs font-bold text-[#ef4444] min-h-[12px]">−57%</span></div>
          </div>
          <div className="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style={{ '--w': '26%', '--c1': '#8b5cf6', '--c2': '#7c3aed' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span className="text-[13px] font-bold whitespace-nowrap">Activated</span><span className="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">3,100</span></div>
            <div className="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span className="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">60%</span><span className="text-xs font-bold text-[#ef4444] min-h-[12px]">−40%</span></div>
          </div>
          <div className="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style={{ '--w': '14%', '--c1': '#a855f7', '--c2': '#9333ea' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span className="text-[13px] font-bold whitespace-nowrap">Subscribed</span><span className="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">1,450</span></div>
            <div className="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span className="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">47%</span><span className="text-xs font-bold text-[#ef4444] min-h-[12px]">−53%</span></div>
          </div>
          <div className="fn-stage flex items-center gap-3 cursor-pointer [transition:opacity_.25s]" style={{ '--w': '9%', '--c1': '#d946ef', '--c2': '#c026d3' }} onClick={(event) => { focusStage(event.currentTarget) }}>
            <div className="fn-bar relative h-12 w-[var(--w)] min-w-[120px] my-0 mx-auto rounded-[11px] [background:linear-gradient(90deg,var(--c1),var(--c2))] flex items-center justify-between py-0 px-3.5 text-[#fff] overflow-hidden [animation:fn-grow_.8s_cubic-bezier(.4,0,.2,1)_both] [transition:box-shadow_.2s]"><span className="text-[13px] font-bold whitespace-nowrap">Renewed</span><span className="text-[13px] font-extrabold [font-variant-numeric:tabular-nums] whitespace-nowrap ml-2.5">870</span></div>
            <div className="w-[74px] shrink-0 text-right flex flex-col leading-tight"><span className="text-sm font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]">60%</span><span className="text-xs font-bold text-[#ef4444] min-h-[12px]">−40%</span></div>
          </div>
        </div>
      
        <div className="fn-foot mt-[18px] pt-3.5 border-t border-t-[#f1f5f9] text-xs text-[#64748b]">Overall conversion <strong>7.25%</strong> · click a stage to focus</div>
      </div>
    </>
  );
}
Vue
<template>
  <div class="fn-card">
    <div class="fn-head">
      <h2 class="fn-title">Conversion funnel</h2>
      <span class="fn-sub">Last 30 days</span>
    </div>
  
    <div class="fn-chart" id="fnChart">
      <div class="fn-stage" style="--w:100%;--c1:#6366f1;--c2:#4f46e5" @click="focusStage($event.currentTarget)">
        <div class="fn-bar"><span class="fn-name">Visitors</span><span class="fn-count">12,000</span></div>
        <div class="fn-rate"><span class="fn-pct">100%</span><span class="fn-drop">&nbsp;</span></div>
      </div>
      <div class="fn-stage" style="--w:43%;--c1:#7c83f4;--c2:#6366f1" @click="focusStage($event.currentTarget)">
        <div class="fn-bar"><span class="fn-name">Sign-ups</span><span class="fn-count">5,200</span></div>
        <div class="fn-rate"><span class="fn-pct">43%</span><span class="fn-drop">−57%</span></div>
      </div>
      <div class="fn-stage" style="--w:26%;--c1:#8b5cf6;--c2:#7c3aed" @click="focusStage($event.currentTarget)">
        <div class="fn-bar"><span class="fn-name">Activated</span><span class="fn-count">3,100</span></div>
        <div class="fn-rate"><span class="fn-pct">60%</span><span class="fn-drop">−40%</span></div>
      </div>
      <div class="fn-stage" style="--w:14%;--c1:#a855f7;--c2:#9333ea" @click="focusStage($event.currentTarget)">
        <div class="fn-bar"><span class="fn-name">Subscribed</span><span class="fn-count">1,450</span></div>
        <div class="fn-rate"><span class="fn-pct">47%</span><span class="fn-drop">−53%</span></div>
      </div>
      <div class="fn-stage" style="--w:9%;--c1:#d946ef;--c2:#c026d3" @click="focusStage($event.currentTarget)">
        <div class="fn-bar"><span class="fn-name">Renewed</span><span class="fn-count">870</span></div>
        <div class="fn-rate"><span class="fn-pct">60%</span><span class="fn-drop">−40%</span></div>
      </div>
    </div>
  
    <div class="fn-foot">Overall conversion <strong>7.25%</strong> · click a stage to focus</div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

function focusStage(el) {
  var stages = el.parentElement.querySelectorAll('.fn-stage');
  var wasActive = el.classList.contains('active');
  stages.forEach(function (s) { s.classList.remove('active', 'dim'); });
  if (!wasActive) {
    el.classList.add('active');
    stages.forEach(function (s) { if (s !== el) s.classList.add('dim'); });
  }
}

</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.fn-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:440px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.fn-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:20px}
.fn-title{font-size:17px;font-weight:800;color:#1e293b}
.fn-sub{font-size:12px;color:#94a3b8;font-weight:600}

.fn-chart{display:flex;flex-direction:column;gap:9px}
.fn-stage{display:flex;align-items:center;gap:12px;cursor:pointer;transition:opacity .25s}
.fn-stage.dim{opacity:.32}
.fn-stage.active .fn-bar{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--c2),0 8px 18px rgba(99,102,241,.3)}

.fn-bar{position:relative;height:48px;width:var(--w);min-width:120px;margin:0 auto;border-radius:11px;background:linear-gradient(90deg,var(--c1),var(--c2));display:flex;align-items:center;justify-content:space-between;padding:0 14px;color:#fff;overflow:hidden;animation:fn-grow .8s cubic-bezier(.4,0,.2,1) both;transition:box-shadow .2s}
.fn-stage:nth-child(1) .fn-bar{animation-delay:.05s}
.fn-stage:nth-child(2) .fn-bar{animation-delay:.15s}
.fn-stage:nth-child(3) .fn-bar{animation-delay:.25s}
.fn-stage:nth-child(4) .fn-bar{animation-delay:.35s}
.fn-stage:nth-child(5) .fn-bar{animation-delay:.45s}
@keyframes fn-grow{from{width:0;opacity:0}to{width:var(--w);opacity:1}}
.fn-name{font-size:13px;font-weight:700;white-space:nowrap}
.fn-count{font-size:13px;font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap;margin-left:10px}

.fn-rate{width:74px;flex-shrink:0;text-align:right;display:flex;flex-direction:column;line-height:1.25}
.fn-pct{font-size:14px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
.fn-drop{font-size:10px;font-weight:700;color:#ef4444;min-height:12px}

.fn-foot{margin-top:18px;padding-top:14px;border-top:1px solid #f1f5f9;font-size:12px;color:#64748b}
.fn-foot strong{color:#6366f1;font-weight:800}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-funnel-chart',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="fn-card">
      <div class="fn-head">
        <h2 class="fn-title">Conversion funnel</h2>
        <span class="fn-sub">Last 30 days</span>
      </div>
    
      <div class="fn-chart" id="fnChart">
        <div class="fn-stage" style="--w:100%;--c1:#6366f1;--c2:#4f46e5" (click)="focusStage($event.currentTarget)">
          <div class="fn-bar"><span class="fn-name">Visitors</span><span class="fn-count">12,000</span></div>
          <div class="fn-rate"><span class="fn-pct">100%</span><span class="fn-drop">&nbsp;</span></div>
        </div>
        <div class="fn-stage" style="--w:43%;--c1:#7c83f4;--c2:#6366f1" (click)="focusStage($event.currentTarget)">
          <div class="fn-bar"><span class="fn-name">Sign-ups</span><span class="fn-count">5,200</span></div>
          <div class="fn-rate"><span class="fn-pct">43%</span><span class="fn-drop">−57%</span></div>
        </div>
        <div class="fn-stage" style="--w:26%;--c1:#8b5cf6;--c2:#7c3aed" (click)="focusStage($event.currentTarget)">
          <div class="fn-bar"><span class="fn-name">Activated</span><span class="fn-count">3,100</span></div>
          <div class="fn-rate"><span class="fn-pct">60%</span><span class="fn-drop">−40%</span></div>
        </div>
        <div class="fn-stage" style="--w:14%;--c1:#a855f7;--c2:#9333ea" (click)="focusStage($event.currentTarget)">
          <div class="fn-bar"><span class="fn-name">Subscribed</span><span class="fn-count">1,450</span></div>
          <div class="fn-rate"><span class="fn-pct">47%</span><span class="fn-drop">−53%</span></div>
        </div>
        <div class="fn-stage" style="--w:9%;--c1:#d946ef;--c2:#c026d3" (click)="focusStage($event.currentTarget)">
          <div class="fn-bar"><span class="fn-name">Renewed</span><span class="fn-count">870</span></div>
          <div class="fn-rate"><span class="fn-pct">60%</span><span class="fn-drop">−40%</span></div>
        </div>
      </div>
    
      <div class="fn-foot">Overall conversion <strong>7.25%</strong> · click a stage to focus</div>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .fn-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:440px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
    .fn-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:20px}
    .fn-title{font-size:17px;font-weight:800;color:#1e293b}
    .fn-sub{font-size:12px;color:#94a3b8;font-weight:600}
    
    .fn-chart{display:flex;flex-direction:column;gap:9px}
    .fn-stage{display:flex;align-items:center;gap:12px;cursor:pointer;transition:opacity .25s}
    .fn-stage.dim{opacity:.32}
    .fn-stage.active .fn-bar{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--c2),0 8px 18px rgba(99,102,241,.3)}
    
    .fn-bar{position:relative;height:48px;width:var(--w);min-width:120px;margin:0 auto;border-radius:11px;background:linear-gradient(90deg,var(--c1),var(--c2));display:flex;align-items:center;justify-content:space-between;padding:0 14px;color:#fff;overflow:hidden;animation:fn-grow .8s cubic-bezier(.4,0,.2,1) both;transition:box-shadow .2s}
    .fn-stage:nth-child(1) .fn-bar{animation-delay:.05s}
    .fn-stage:nth-child(2) .fn-bar{animation-delay:.15s}
    .fn-stage:nth-child(3) .fn-bar{animation-delay:.25s}
    .fn-stage:nth-child(4) .fn-bar{animation-delay:.35s}
    .fn-stage:nth-child(5) .fn-bar{animation-delay:.45s}
    @keyframes fn-grow{from{width:0;opacity:0}to{width:var(--w);opacity:1}}
    .fn-name{font-size:13px;font-weight:700;white-space:nowrap}
    .fn-count{font-size:13px;font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap;margin-left:10px}
    
    .fn-rate{width:74px;flex-shrink:0;text-align:right;display:flex;flex-direction:column;line-height:1.25}
    .fn-pct{font-size:14px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
    .fn-drop{font-size:10px;font-weight:700;color:#ef4444;min-height:12px}
    
    .fn-foot{margin-top:18px;padding-top:14px;border-top:1px solid #f1f5f9;font-size:12px;color:#64748b}
    .fn-foot strong{color:#6366f1;font-weight:800}
  `]
})
export class FunnelChartComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    function focusStage(el) {
      var stages = el.parentElement.querySelectorAll('.fn-stage');
      var wasActive = el.classList.contains('active');
      stages.forEach(function (s) { s.classList.remove('active', 'dim'); });
      if (!wasActive) {
        el.classList.add('active');
        stages.forEach(function (s) { if (s !== el) s.classList.add('dim'); });
      }
    }

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { focusStage });
  }
}