Pila de tarjetas en abanico
Etapas apiladas que se abren en abanico al apuntarlas o enfocarlas; clic envía la tarjeta al fondo de la pila.
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";
/** Pila de tarjetas que se abre en abanico al apuntarla o enfocarla; clic en una la trae al frente. Estilo minimalista. React + TypeScript, sin dependencias. */
export type TarjetaAbanicoItem = { id: string; title: string; meta: string; color: string; ink?: string };
export type TarjetaAbanicoProps = {
items?: TarjetaAbanicoItem[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gctab-root { box-sizing: border-box; display: block; width: 100%; max-width: 360px; font: 500 15px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gctab-root *, .gctab-root *::before, .gctab-root *::after { box-sizing: border-box; }
.gctab-root button { font: inherit; }
@media (prefers-reduced-motion: reduce) { .gctab-root *, .gctab-root *::before, .gctab-root *::after { transition-duration: .01ms !important; } }
.gctab-stack { position: relative; height: 270px; outline: 0; }
.gctab-stack:focus-within .gctab-card, .gctab-stack:hover .gctab-card { --open: 1; }
.gctab-card { --open: 0; position: absolute; left: 50%; top: 40px; width: 220px; height: 190px; display: flex; flex-direction: column; justify-content: space-between; padding: 18px; text-align: left; border: 0; border-radius: 20px; cursor: pointer; background: var(--bg); color: var(--ink, #111); box-shadow: 0 1px 0 rgba(255, 255, 255, .6) inset, 0 14px 24px -12px rgba(0, 0, 0, .35); transform-origin: 50% 120%; transform: translateX(-50%) translateY(calc(var(--i) * -6px)) translateX(calc(var(--open) * var(--s) * 62px)) rotate(calc(var(--open) * var(--s) * 9deg)); transition: transform .6s cubic-bezier(.16, 1, .3, 1); }
.gctab-card:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
.gctab-title { font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
.gctab-meta { font-size: 13px; opacity: .72; }`;
const DEFAULT_ITEMS: TarjetaAbanicoItem[] = [
{ id: "1", title: "Diagnóstico", meta: "30 – 45 min · sin costo", color: "#ffe0d2" },
{ id: "2", title: "Propuesta", meta: "Alcance, tiempos y precio", color: "#d8ecff" },
{ id: "3", title: "Desarrollo", meta: "Sprints de 1 a 2 semanas", color: "#e0f4d6" },
{ id: "4", title: "Lanzamiento", meta: "30 días de estabilización", color: "#fff1b8" },
];
export function TarjetaAbanico({ items = DEFAULT_ITEMS, className = "", style }: TarjetaAbanicoProps) {
const [order, setOrder] = useState(items.map((item) => item.id));
const byId = Object.fromEntries(items.map((item) => [item.id, item]));
const mid = (order.length - 1) / 2;
return (
<div className={`gctab-root ${className}`} style={style}>
<style>{CSS}</style>
<div className="gctab-stack" role="group" aria-label="Etapas del proyecto">
{order.map((id, index) => {
const item = byId[id];
return (
<button
key={id}
className="gctab-card"
type="button"
style={{ "--bg": item.color, "--ink": item.ink, "--i": index, "--s": index - mid, zIndex: index } as CSSProperties}
onClick={() => setOrder((list) => [...list.filter((value) => value !== id), id])}
>
<span className="gctab-title">{item.title}</span>
<span className="gctab-meta">{item.meta}</span>
</button>
);
})}
</div>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/tarjeta-abanicoEjemplo de uso de Pila de tarjetas en abanico
Después de copiar tarjeta-abanico.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { TarjetaAbanico } from "./tarjeta-abanico";
export default function Ejemplo() {
return <TarjetaAbanico />;
}Propiedades de TarjetaAbanico
TarjetaAbanico acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
items? | TarjetaAbanicoItem[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Pila de tarjetas en abanico con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--ink | #111 |
Accesibilidad y rendimiento
El archivo tiene 60 líneas (3 KB) y no necesita @keyframes. Esto es lo que hace Pila de tarjetas en abanico 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).
- Declara roles ARIA para lectores de pantalla: group.
- Es un componente de cliente de Next.js: incluye la directiva "use client".