More Charts Snippets
Funnel Chart — Conversion Funnel HTML CSS JS Snippet
Funnel Chart · Charts · Plain HTML, CSS & JS · Live preview
What's included
Features
--w custom property and centred with margin: 0 auto, forming the tapering funnel with no SVG or clip-path.min-width keeps even the narrowest stage wide enough to show its label and count, so the funnel base stays legible.fn-grow keyframe animates each bar from zero to var(--w) with nth-child delays, so the funnel pours in top to bottom.var(--w) lets one keyframe rule drive every stage regardless of its target width — no per-bar animation needed.focusStage rings the selected bar and dims the rest, then clears on a second click — built from two classes and one function.--c1/--c2 for its own gradient, giving the funnel a cohesive hue progression down the steps.About this UI Snippet
Funnel Chart — Centred Tapering Bars, Stage Conversion Rates & Drop-Off Indicators

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
- 1Paste HTML, CSS, and JSA "Conversion funnel" card appears and five centred bars (Visitors → Renewed) grow in from top to bottom, forming a tapering funnel.
- 2Read the stagesEach bar shows its name and count; its width is proportional to the top stage, so the narrowing visualises drop-off.
- 3Check 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).
- 4Click a stageClick any bar — it highlights with a ring and the others dim, letting you focus a single step. Click it again to clear.
- 5See overall conversionThe footer shows the end-to-end rate (7.25%), the fraction of visitors who reached the final stage.
- 6Plug 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
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
<!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"> </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><!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]"> </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>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"> </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>
</>
);
}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]"> </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>
</>
);
}<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"> </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>// @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"> </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 });
}
}