Cómo crear un efecto de cartas apiladas en abanico en React con Framer Motion
Un abanico de cartas apiladas en React coloca hasta 5 cartas en posición absoluta dentro de un contenedor de altura fija, cada una desplazada por una pequeña rotación calculada a partir de su índice. Al pasar el cursor, Framer Motion anima cada carta hacia una rotación más amplia y una traslación en X, desplegándolas en abanico. Todo el efecto se apoya en una sola prop animate gobernada por un booleano isExpanded.
- Stack: React + Framer Motion 11 + lucide-react, ~137 líneas, cero dependencias adicionales.
- API clave: useState, motion.a, AnimatePresence, prop animate con rotate/x/scale.
- Cada carta es una etiqueta anchor, por lo que la sección es navegable con teclado y compatible con lectores de pantalla.
- Salvedad en móvil: el despliegue del abanico depende de mouseenter/mouseleave, que no se disparan en pantallas táctiles.
- Admite de 3 a 5 artículos; el cálculo del punto medio centra el reparto de la rotación de forma simétrica.
Blog Card Stack es una sección de blog en React que presenta los artículos como una baraja física de cartas. Las cartas reposan apiladas con suaves rotaciones alternas, y un simple paso del cursor las despliega en un abanico que el lector puede ojear. Convierte una lista de entradas en un momento táctil y lleno de curiosidad, sin necesidad de scroll.
Anatomía
Un encabezado de sección centrado (etiqueta eyebrow + título serif en cursiva) corona un contenedor de 420px de alto. Dentro, hasta 5 cartas motion.a se posicionan de forma absoluta, todas compartiendo el mismo inset:0 para ocupar el mismo espacio. Cada carta tiene una imagen de fondo con baja opacidad, una superficie de carta, una insignia de categoría, el título del artículo con un icono ArrowUpRight, un extracto y una fecha. Las cartas se apilan visualmente mediante z-index y un box-shadow por carta con un desenfoque creciente.
Cómo funciona
La lógica de apilado y abanico reside por completo en la prop animate de cada motion.a. Para una lista de N cartas, el punto medio es (N-1)/2. En el estado cerrado, cada carta gira (i - mid) * 3 grados y se queda en x:0. En el estado abierto, la rotación se amplía a (i - mid) * 8 grados y x salta a (i - mid) * 120px. La escala disminuye ligeramente con la profundidad (1 - i * 0.02) cuando está cerrado y se fija en 0.88 para todas cuando está abierto. Un array de easing personalizado [0.16, 1, 0.3, 1] da a la transición un carácter ágil con un aterrizaje suave. Todo el cambio de estado ocurre en onMouseEnter/onMouseLeave del contenedor padre.
Cómo crearlo en React
Preparar el contenedor y el estado de expansión
Crea un contenedor relative con una altura fija (420px funciona bien para cartas verticales) y un maxWidth centrado con margin auto. Añade onMouseEnter y onMouseLeave para alternar un único booleano isExpanded. Posiciona todas las cartas hijas de forma absoluta para que ocupen la misma huella.
const [isExpanded, setIsExpanded] = useState(false); <div onMouseEnter={() => setIsExpanded(true)} onMouseLeave={() => setIsExpanded(false)} style={{ position: "relative", height: "420px", maxWidth: "640px", margin: "0 auto" }} >Calcular la rotación y la traslación de cada carta
Para cada carta en el índice i, calcula un valor mid como (items.length - 1) / 2. La rotación de apilado es (i - mid) * 3 grados; la rotación del abanico es (i - mid) * 8 grados. El desplazamiento X del abanico es (i - mid) * 120px, lo que da unos 120px entre los centros de las cartas una vez desplegadas. Estas fórmulas centran el reparto de forma simétrica sin importar el número de cartas.
const mid = (items.length - 1) / 2; const stackRotate = (i - mid) * 3; const fanRotate = (i - mid) * 8; const fanX = (i - mid) * 120;Gobernar la prop animate con isExpanded
Pasa un objeto animate a cada motion.a que lea isExpanded para elegir entre los valores de apilado y de abanico. Añade un retraso escalonado de i * 0.05 segundos para que las cartas se desplieguen en cascada en lugar de moverse todas a la vez. El array de easing personalizado [0.16, 1, 0.3, 1] produce un arranque rápido con un final de leve sobreimpulso.
animate={{ rotate: isExpanded ? fanRotate : stackRotate, x: isExpanded ? fanX : 0, scale: isExpanded ? 0.88 : 1 - i * 0.02, zIndex: items.length - i, }} transition={{ duration: 0.5, delay: i * 0.05, ease: [0.16, 1, 0.3, 1] }}Añadir la entrada whileInView y el lift whileHover
Fija initial en opacity:0, y:40, rotate:0 y whileInView en opacity:1, y:0, rotate:stackRotate para que las cartas se animen al entrar en scroll con independencia del estado de hover. Un simple whileHover de y:-8 eleva ligeramente la carta sobre la que está el cursor, reforzando la metáfora de la baraja física. Envuelve todo en AnimatePresence para gestionar el renderizado condicional de forma limpia.
initial={{ opacity: 0, y: 40, rotate: 0 }} whileInView={{ opacity: 1, y: 0, rotate: stackRotate }} whileHover={{ y: -8 }}
Cuándo usarlo
Blog Card Stack funciona bien en la parte central o final de una landing page de agencia, SaaS o editorial, cuando quieres destacar de 3 a 5 artículos recientes sin una cuadrícula convencional. La interacción invita a la curiosidad sin imponerla. Evítalo en un índice de blog dedicado donde los lectores esperan filas fáciles de ojear, y prevé siempre un fallback adaptado al tacto, ya que el despliegue en abanico no se activa en móvil.
Usado por
- Linear, Utiliza superficies superpuestas tipo carta con revelados escalonados en sus secciones de funcionalidades de producto para crear profundidad sin recargar el diseño.
- Stripe, Muestra motivos de baraja de cartas superpuestas en sus páginas de producto para desarrolladores, para comunicar varios elementos paralelos sin una lista plana.
- Craft, Emplea animaciones de cartas de documento apiladas en su landing page para transmitir la idea de notas superpuestas y páginas enlazadas.
FAQ
¿Funciona el efecto abanico en móvil?
No. El despliegue depende de los eventos mouseenter y mouseleave, que no se disparan en pantallas táctiles. En móvil, deja las cartas en su estado apilado o sustituye la interacción por un toggle de tocar para expandir.
¿Cuántas cartas puede manejar la pila?
El componente recorta el array de artículos a los 5 primeros elementos. Más allá de 5, las cartas en el estado apilado empiezan a solaparse de forma incómoda y el despliegue en abanico a intervalos de 120px supera el contenedor de 640px. De tres a cinco cartas da el mejor resultado visual.
¿Por qué usar AnimatePresence aquí?
AnimatePresence gestiona el caso en que el array de artículos cambia dinámicamente, asegurando que las cartas eliminadas salgan de forma limpia en lugar de desaparecer al instante. Para un array estático añade poca sobrecarga, pero mantiene el componente robusto ante escenarios reales de carga de datos.
¿Puedo sustituir el truco de profundidad con box-shadow por otra cosa?
Sí. Una alternativa habitual es variar el filtro de brillo (filter: brightness(0.9 - i * 0.05)) para oscurecer las cartas más al fondo de la pila, simulando la sombra sin una sombra proyectada dura. Puedes combinar ambos para una sensación de profundidad física más marcada.