Embudo de conversión de vidrio
Etapas centradas con ancho proporcional, porcentaje de paso a la siguiente y detalle al enfocar. Glassmorfismo.
Del primer clic al proyecto
- ↓ 25% pasa a la siguiente etapa
- ↓ 40% pasa a la siguiente etapa
- ↓ 33% pasa a la siguiente etapa
- ↓ 23% pasa a la siguiente etapa
Apunta a una etapa para ver su peso sobre el total.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties } from "react";
/** Embudo de conversión en vidrio esmerilado: cada etapa es una barra centrada cuyo ancho es proporcional al valor, con porcentaje respecto a la etapa anterior y detalle al enfocar. Datos de muestra. React + TypeScript, sin dependencias. */
export type DatosEmbudoEtapa = { label: string; value: number };
export type DatosEmbudoProps = {
stages?: DatosEmbudoEtapa[];
title?: string;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcdem-root { box-sizing: border-box; width: 100%; max-width: 520px; padding: 22px; border-radius: 24px; background: rgba(255, 255, 255, .14); -webkit-backdrop-filter: blur(20px) saturate(1.7); backdrop-filter: blur(20px) saturate(1.7); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .3), inset 0 1px 0 rgba(255, 255, 255, .55), 0 28px 44px -22px rgba(10, 10, 60, .6); color: #fff; font: 500 14px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; text-shadow: 0 1px 2px rgba(10, 10, 60, .25); }
.gcdem-root *, .gcdem-root *::before, .gcdem-root *::after { box-sizing: border-box; }
.gcdem-root button { font: inherit; }
.gcdem-root :focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcdem-root * { transition-duration: .01ms !important; animation: none !important; } }
.gcdem-title { margin: 0 0 16px; font-size: 16px; font-weight: 700; }
.gcdem-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.gcdem-row { display: grid; gap: 4px; justify-items: center; }
.gcdem-bar { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 12px; width: var(--w); min-width: min(100%, 230px); white-space: nowrap; height: 46px; padding: 0 16px; border: 0; border-radius: 12px; background: linear-gradient(180deg, rgba(255, 255, 255, .38), rgba(255, 255, 255, .18)); color: #fff; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .4), inset 0 1px 0 rgba(255, 255, 255, .7); cursor: pointer; text-shadow: 0 1px 2px rgba(10, 10, 60, .35); transition: transform .3s cubic-bezier(.16, 1, .3, 1), background .2s; animation: gcdem-in .7s cubic-bezier(.16, 1, .3, 1) backwards; }
.gcdem-bar:hover, .gcdem-bar[data-on="true"] { transform: scaleX(1.03); background: linear-gradient(180deg, rgba(255, 255, 255, .5), rgba(255, 255, 255, .26)); }
.gcdem-bar b { font-variant-numeric: tabular-nums; }
.gcdem-conv { font-size: 12px; opacity: .85; font-variant-numeric: tabular-nums; }
.gcdem-detail { min-height: 20px; margin: 14px 0 0; font-size: 13px; opacity: .9; }
@keyframes gcdem-in { from { opacity: 0; transform: scaleX(.5); } }`;
const DEFAULT: DatosEmbudoEtapa[] = [
{ label: "Visitas", value: 12400 },
{ label: "Diagnósticos iniciados", value: 3100 },
{ label: "Diagnósticos enviados", value: 1240 },
{ label: "Llamadas", value: 410 },
{ label: "Proyectos", value: 96 },
];
export function DatosEmbudo({ stages = DEFAULT, title = "Del primer clic al proyecto", className = "", style }: DatosEmbudoProps) {
const [on, setOn] = useState<number | null>(null);
const top = stages[0].value;
const n = new Intl.NumberFormat("es-MX");
return (
<section className={`gcdem-root ${className}`} style={style} aria-label={title}>
<style>{CSS}</style>
<h3 className="gcdem-title">{title}</h3>
<ol className="gcdem-list">
{stages.map((stage, i) => {
const prev = i ? stages[i - 1].value : null;
return (
<li key={stage.label} className="gcdem-row">
{prev ? <span className="gcdem-conv">↓ {Math.round((stage.value / prev) * 100)}% pasa a la siguiente etapa</span> : null}
<button className="gcdem-bar" type="button" data-on={on === i} style={{ "--w": `${Math.max(24, (stage.value / top) * 100)}%`, animationDelay: `${i * 0.08}s` } as CSSProperties} onPointerEnter={() => setOn(i)} onPointerLeave={() => setOn(null)} onFocus={() => setOn(i)} onBlur={() => setOn(null)}>
<span>{stage.label}</span>
<b>{n.format(stage.value)}</b>
</button>
</li>
);
})}
</ol>
<p className="gcdem-detail" aria-live="polite">{on !== null ? `${stages[on].label}: ${Math.round((stages[on].value / top) * 1000) / 10}% del total de visitas.` : "Apunta a una etapa para ver su peso sobre el total."}</p>
</section>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/datos-embudoEjemplo de uso de Embudo de conversión de vidrio
Después de copiar datos-embudo.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { DatosEmbudo } from "./datos-embudo";
export default function Ejemplo() {
return <DatosEmbudo title="Ejemplo" />;
}Propiedades de DatosEmbudo
DatosEmbudo acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
stages? | DatosEmbudoEtapa[] | Opcional. |
title? | string | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 65 líneas (4 KB) y define 1 animación con @keyframes. Esto es lo que hace Embudo de conversión de vidrio por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Usa backdrop-filter para el efecto de vidrio; en navegadores sin soporte el panel se ve sin desenfoque.
- Es un componente de cliente de Next.js: incluye la directiva "use client".