Retour au catalogue

Blog Hover Preview

Liste minimaliste de titres d'articles. Au hover, un preview card (image + extrait) apparait pres du curseur.

blogcomplex Both Responsive a11y
minimaleditorialagencysaasportfoliostacked
Theme

Cómo crear una lista de artículos React con preview flotante al hover

Un preview al hover en React sigue el cursor con useMotionValue, guarda el índice de la fila activa en un useState y monta una tarjeta flotante (imagen + extracto) mediante AnimatePresence junto al puntero. La tarjeta entra con scale desde 0.92 y se desvanece cuando la fila pierde el foco, dando una sensación editorial fluida.

  • Stack: React 18 + Framer Motion 11 + Lucide React, ~160 líneas, cero dependencias adicionales.
  • API principal: useState, useMotionValue, AnimatePresence, motion.a.
  • La tarjeta de preview usa pointerEvents:none para no bloquear nunca los eventos de ratón de la fila.
  • Accesible: cada fila es una etiqueta anchor real; el preview flotante es decorativo y queda oculto a los lectores de pantalla.
  • En dispositivos táctiles la lista se muestra sin preview (no hay estado hover en móvil).

Blog Hover Preview es una lista de artículos minimalista donde cada fila, al pasar el cursor, hace aparecer una pequeña tarjeta flotante junto al puntero con la imagen y el extracto del artículo. El layout se mantiene limpio y tipográfico en reposo; la tarjeta de preview aporta profundidad sin recargar la interfaz. Es el patrón que usan los sitios editoriales y de agencias que buscan un acabado de alta gama sin carruseles pesados.

Anatomía

La sección tiene tres partes estructurales. Un motion header contiene la etiqueta de sección y un título serif en cursiva, ambos entrando con una animación de opacity/y. Debajo, un contenedor relative aloja las filas de artículos y la tarjeta de preview flotante. Cada fila es un elemento motion.a maquetado como flex row: un número de índice en monospace, un título en negrita (que se desplaza 12px hacia la derecha al hover), una etiqueta de categoría y un icono ArrowUpRight que rota 45 grados al hover. La tarjeta de preview flotante es un motion.div en absolute posicionado mediante las coordenadas de useMotionValue, que contiene una miniatura en background-image y un bloque de texto con extracto y fecha.

Cómo funciona

Todo el efecto funciona a partir de dos motion values (mouseX, mouseY) y un único estado (hoveredIndex). En cada mousemove sobre el contenedor de filas, getBoundingClientRect convierte las coordenadas del puntero en valores locales al contenedor y las escribe en mouseX/mouseY. Estos valores alimentan directamente las propiedades left/top de la tarjeta de preview en absolute, de modo que se mantiene junto al cursor sin ningún re-render. Cuando se entra en una fila, setHoveredIndex se dispara y AnimatePresence monta la tarjeta con una entrada de scale(0.92) hacia scale(1). Al salir de la fila, hoveredIndex vuelve a null y AnimatePresence desmonta la tarjeta con la transición inversa. El desplazamiento del título usa un motion.h3 aparte que anima su propiedad x entre 0 y 12 según si esa fila es la que está activa.

Cómo crearlo en React

  1. Configura las motion values y el índice activo

    Crea mouseX y mouseY con useMotionValue(0) y un estado hoveredIndex inicializado a null. En el div contenedor de las filas, añade un handler onMouseMove que lea la posición del puntero relativa al contenedor y la escriba en las dos motion values.

    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
    
    const handleMouseMove = (e: React.MouseEvent) => {
      const rect = e.currentTarget.getBoundingClientRect();
      mouseX.set(e.clientX - rect.left + 24);
      mouseY.set(e.clientY - rect.top - 160);
    };
  2. Construye la fila de artículo con estado hover

    Cada fila es un motion.a. En mouseEnter escribe el índice de la fila en hoveredIndex, en mouseLeave vuelve a null. Dentro de la fila, envuelve el título en un motion.h3 y anima su x entre 0 y 12 según si este índice coincide con hoveredIndex. Haz lo mismo para la rotación del icono ArrowUpRight.

    <motion.h3
      animate={{ x: hoveredIndex === i ? 12 : 0 }}
      transition={{ duration: 0.3, ease: EASE }}
    >
      {article.title}
    </motion.h3>
  3. Monta la tarjeta flotante con AnimatePresence

    Dentro del contenedor relative, después de las filas, añade un bloque AnimatePresence. Cuando hoveredIndex no es null, renderiza un motion.div posicionado en absolute con style left y top ligados a mouseX y mouseY. Dale un scale inicial de 0.92 y opacidad 0, anímalo hacia scale 1 y opacidad 1, y pon pointerEvents en none para que nunca interfiera con las filas de debajo.

    <AnimatePresence>
      {hoveredIndex !== null && (
        <motion.div
          initial={{ opacity: 0, scale: 0.92 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.92 }}
          transition={{ duration: 0.2 }}
          style={{
            position: "absolute",
            left: mouseX,
            top: mouseY,
            pointerEvents: "none",
            zIndex: 40,
          }}
        />
      )}
    </AnimatePresence>
  4. Rellena la tarjeta con la imagen y el extracto

    Dentro del motion.div, renderiza un div de altura fija con backgroundImage establecido en la URL de imagen del artículo activo, seguido de un bloque con padding que muestra el extracto y la fecha. Mantén el ancho de la tarjeta en torno a 280px y añade un box-shadow para dar profundidad. La tarjeta siempre reflejará el artículo activo en cada momento porque lee de articles[hoveredIndex].

Cuándo usarlo

Usa este patrón en secciones de blog de agencias, portfolios o SaaS donde quieras que la lista de artículos resulte editorial y premium. Funciona mejor con 4 a 8 artículos, cada uno con una imagen atractiva. Evítalo cuando los artículos carecen de imágenes de calidad, cuando tu audiencia es mayoritariamente móvil (el hover no se dispara en táctil), o cuando la lista es lo bastante larga como para que una cuadrícula o un layout de tarjetas ofrezca mejor legibilidad.

Usado por

  • Stripe, Usa una lista de artículos apilada minimalista con jerarquía tipográfica e interacciones sutiles en las filas de su blog de ingeniería.
  • Vercel, Listado de artículos limpio con resaltados al hover y metadatos de preview enriquecidos que aparecen al interactuar con la fila.
  • Linear, Emplea listas de artículos basadas en filas con transiciones de estado al hover que muestran contexto adicional junto al puntero.

FAQ

¿Funciona el preview al hover en móvil?

No. Las pantallas táctiles no disparan eventos hover ni mousemove, por lo que la tarjeta flotante nunca aparece. La lista de artículos en sí es totalmente funcional en móvil; solo falta el preview decorativo.

¿Por qué usar useMotionValue en lugar de useState para la posición del cursor?

useMotionValue actualiza el style del DOM directamente sin disparar un re-render de React en cada evento mousemove. Con useState, un cursor rápido encolaría decenas de renders por segundo y haría que la tarjeta diera tirones; useMotionValue mantiene la tarjeta fija al cursor a 60fps.

¿Cómo evito que la tarjeta se desborde por el borde del viewport?

Añade un clamp dentro de handleMouseMove: limita mouseX para que x + cardWidth nunca supere el ancho del contenedor, y limita mouseY de forma simétrica. Un simple Math.min en ambos ejes basta en la mayoría de los casos.

¿Puedo reemplazar la imagen por una vista previa en vídeo?

Sí. Sustituye el div con background-image por un elemento de vídeo en silencio con reproducción automática. Combínalo con una prop key igual a la URL del artículo para que React desmonte y vuelva a montar el elemento de vídeo cada vez que se pasa el cursor por una nueva fila, reiniciando la reproducción desde el principio.

"use client";

import { useState } from "react";
import { motion, AnimatePresence, useMotionValue } from "framer-motion";
import { ArrowUpRight } from "lucide-react";

interface Article {
  title: string;
  excerpt: string;
  category: string;
  date: string;
  image: string;
  url: string;
}

interface BlogHoverPreviewProps {
  heading?: string;
  subtitle?: string;
  articles?: Article[];
}

const EASE = [0.16, 1, 0.3, 1] as const;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reseñas

Lista de artículos React con tarjeta de preview al hover,