Cómo crear una lista de blog animada y minimalista en React
Una lista de blog en React minimalista muestra los artículos como filas animadas mediante whileInView de Framer Motion con un retardo por elemento, produciendo un efecto de entrada escalonada. Cada fila contiene una fecha, un título con icono de flecha y un badge de tag, todo gobernado por propiedades CSS personalizadas para que el componente se adapte a cualquier tema.
- Stack: React + Framer Motion 11 + lucide-react, ~140 líneas, sin otras dependencias.
- Animación: entrada con whileInView desde x:-8, retardo escalonado de 40ms por fila.
- Diseño: cuadrícula CSS con tres columnas (100px fecha / 1fr título / auto tag) para una alineación precisa al píxel.
- Totalmente adaptable al tema mediante propiedades CSS personalizadas; funciona en modo claro y oscuro de forma nativa.
- Accesible: etiquetas anchor semánticas, navegable con el teclado, texto legible sin animación.
Blog Minimal List es una sección React que muestra los artículos en un diseño de filas sobrio y centrado en la tipografía, inspirado en los changelogs de Notion y Linear. Sin miniaturas, sin tarjetas, solo fecha, título y tag en una única línea horizontal separada por un borde fino. Framer Motion desliza cada fila desde la izquierda cuando entra en el viewport, haciendo que una lista larga se sienta ligera en lugar de pesada.
Anatomía
La sección envuelve un contenedor centrado de 720px. Un bloque de cabecera (título h2 + párrafo de subtítulo) se anima primero con un simple fundido y ascenso. Debajo, una columna flex aloja las filas de artículos. Cada fila es un elemento motion.a dispuesto como una cuadrícula CSS: una columna de fecha de 100px en texto atenuado con tabular-nums, una columna de título de 1fr con un pequeño icono ArrowUpRight en línea, y un badge de tag de ancho automático fijado a la derecha. Un border-bottom de 1px separa las filas.
Cómo funciona
Cada motion.a empieza con opacity:0 y x:-8, luego anima hacia opacity:1 y x:0 en cuanto el elemento entra en el viewport (viewport: { once: true }). El retardo es i * 0.04 segundos, así que la primera fila aparece a 0ms, la segunda a 40ms, la tercera a 80ms, creando una cascada natural sin una API de stagger dedicada. La curva de easing [0.16, 1, 0.3, 1] es un cubic bezier de salida rápida y entrada lenta que arranca pronto y se posa con suavidad, evitando la sensación flotante de ease-in-out.
Cómo crearlo en React
Definir el tipo Article y las props del componente
Empieza con una interfaz TypeScript mínima: title (string), date (string), tag (string). Exponlos como una prop articles opcional junto a sectionTitle y sectionSubtitle. Así el componente se mantiene puramente presentacional, sin acoplamiento con el CMS.
interface Article { title: string; date: string; tag: string; }Animar el bloque de cabecera a la entrada
Envuelve el h2 y el subtítulo en una sola motion.div. Usa initial={{ opacity: 0, y: 12 }} y whileInView={{ opacity: 1, y: 0 }} con viewport={{ once: true }}. Una duración de 0.5s con el ease personalizado da una entrada segura sin robar la atención de la lista de abajo.
const EASE = [0.16, 1, 0.3, 1] as const; <motion.div initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, ease: EASE }} >Construir las filas de artículos escalonadas
Recorre articles con map y renderiza cada uno como un motion.a. Define initial={{ opacity: 0, x: -8 }} y el retardo de transición en i * 0.04. La disposición en cuadrícula (gridTemplateColumns: '100px 1fr auto') alinea fecha, título y badge de tag sobre una misma línea base en todas las filas, sin importar la longitud del título.
articles.map((article, i) => ( <motion.a key={i} initial={{ opacity: 0, x: -8 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ duration: 0.35, delay: i * 0.04, ease: EASE }} style={{ display: "grid", gridTemplateColumns: "100px 1fr auto" }} > ... </motion.a> ))Aplicar estilos con propiedades CSS personalizadas para el tema
Usa var(--color-background), var(--color-foreground), var(--color-foreground-muted), var(--color-accent) y var(--color-border) en todo el componente. Nunca codifiques un color en duro. El badge de tag usa var(--color-accent-border) para su contorno, por lo que coincide automáticamente con el preset de tema activo sin ninguna clase condicional.
Cuándo usarlo
Recurre a esta sección en sitios de documentación, blogs personales, changelogs de producto o páginas de marketing SaaS donde el contenido es el producto y el ruido visual distraería. Combina de forma natural con una sección de suscripción a una newsletter o un banner CTA debajo. Evítala cuando los artículos tienen contenido visual fuerte (fotos, ilustraciones) que se beneficiaría más de un diseño en tarjetas o en mosaico.
Usado por
- Linear, Changelog mostrado como una lista minimalista de fecha y título, la inspiración directa de este patrón basado en filas.
- Vercel, Changelog centrado en el texto con etiquetas de tag y fechas en un diseño compacto y apilado, sin imágenes.
- Rauno Freiberg, Índice de artículos personales como una simple lista de fecha y título, sin ninguna decoración, foco total en el contenido.
- Notion Blog, Tags de categoría y fechas de publicación junto a los títulos de los artículos en una lista limpia y alineada en cuadrícula.
FAQ
¿Cómo enlazo cada fila con la URL real de su artículo?
Añade un campo url a la interfaz Article, luego reemplaza el href="#" codificado en duro de motion.a por href={article.url}. Si usas Next.js, sustituye motion.a por motion(Link) para conservar el enrutado del lado del cliente.
¿Puedo desactivar la animación para los usuarios que prefieren menos movimiento?
Sí. Lee el hook useReducedMotion de Framer Motion y asigna condicionalmente initial y animate a un objeto vacío cuando devuelve true. El componente se renderiza entonces al instante sin ninguna transición.
¿Cómo ordeno los artículos por fecha automáticamente?
Pasa el array ya ordenado como prop, el componente no ordena por sí mismo por diseño. Ordena en la capa de datos: articles.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) antes de pasarlos.
¿El diseño aguanta en pantallas estrechas?
La columna de fecha de 100px puede quedar justa a 320px. Un arreglo seguro es ocultar la columna de fecha por debajo de sm con una media query, o plegar la cuadrícula de tres columnas en un diseño apilado de dos filas en móvil usando una @container o una clase de breakpoint.