Ir al contenido
Evaluar mi proyecto
Componentes · TarjetasDisponible

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.

tarjeta-abanico.tsx60 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/tarjeta-abanico

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
items?TarjetaAbanicoItem[]Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.

VariableValor 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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)