Cómo crear una sección Sobre nosotros con timeline vertical en React
Una sección Sobre nosotros con timeline vertical en React renderiza cada hito como una fila de grid (punto + contenido), escalonada con el whileInView de Framer Motion para que los elementos aparezcan en fundido y se deslicen a medida que el usuario hace scroll. Un único div posicionado en absoluto traza la línea de conexión detrás de todos los puntos.
- Stack: React + Framer Motion + lucide-react, ~160 líneas, cero dependencias adicionales.
- Animación: whileInView con once:true, entrada deslizándose en el eje x, retardo escalonado de 0,08s × index.
- Theming: 100% CSS custom properties, background, foreground, accent, border, radius. Sin colores codificados en duro.
- Accesible: la línea de la timeline es aria-hidden, jerarquía semántica de encabezados h2/h3, legible en modo de alto contraste.
- Responsive por defecto, clamp() para el tamaño del titular, grid de una sola columna adaptado a cualquier viewport.
About Story Narrative es una sección React que cuenta la historia de una empresa en forma de timeline vertical. Cada hito se desliza desde la izquierda en el momento en que entra en el viewport, impulsado por el whileInView de Framer Motion. El resultado se percibe editorial e intencional, ideal para páginas Sobre nosotros de agencias, SaaS y startups donde la credibilidad se construye hito a hito.
Anatomía
La sección tiene dos zonas. Arriba, un bloque de cabecera centrado (h2 + párrafo de introducción) que aparece en fundido una sola vez mediante un motion.div. Debajo, un contenedor relative contiene la línea vertical, un div de 2px posicionado en absoluto de arriba abajo en el borde izquierdo, y una columna flex de elementos de timeline. Cada elemento es un grid de 2 columnas: un punto de acento de 42px a la izquierda y un bloque de texto a la derecha (etiqueta de año en color de acento, título h3, párrafo).
Cómo funciona
Cada hito se envuelve en un motion.div con initial={{ opacity: 0, x: -16 }} y whileInView={{ opacity: 1, x: 0 }}. La opción viewport once:true hace que la animación se dispare una sola vez y se mantenga, así que quienes hacen scroll rápido nunca ven una repetición. El retardo se calcula como i * 0.08, dando un escalonado natural sin boilerplate de orquestación. El bloque de cabecera usa y: 16 en lugar de x: -16 para distinguirse visualmente. Ambos usan el mismo EASE cubic-bezier [0.16, 1, 0.3, 1] para una sensación ágil y con ligero rebote.
Cómo crearlo en React
Definir la estructura de los datos y renderizar la cabecera
Crea una interface TimelineEvent con los campos year, title y description. Acepta sectionTitle, intro y timeline como props con valores por defecto. Envuelve el h2 + párrafo en un motion.div que aparece en fundido desde y:16 con viewport once:true. Usa clamp() para el font-size del titular para que escale sin breakpoints.
interface TimelineEvent { year: string; title: string; description: string; } <motion.div initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }} > <h2 style={{ fontSize: "clamp(1.75rem, 3vw, 2.5rem)" }}> {sectionTitle} </h2> </motion.div>Trazar la línea vertical y el punto
Dentro de un contenedor relative, coloca un div de 2px de ancho posicionado en absoluto que abarque toda la altura en el lado izquierdo. Esta es la línea de conexión. Cada elemento de timeline es un grid CSS de 2 columnas: 42px para la columna del punto, 1fr para el contenido. El punto es un círculo flex con un borde y relleno de acento usando el icono Circle de lucide-react a 10px.
<div style={{ position: "relative", maxWidth: 680, margin: "0 auto" }}> {/* Connector line */} <div aria-hidden style={{ position: "absolute", left: 20, top: 0, bottom: 0, width: 2, background: "var(--color-border)" }} /> {/* Items */} <div style={{ display: "flex", flexDirection: "column", gap: "2.5rem" }}> {timeline.map((event, i) => ( <div key={i} style={{ display: "grid", gridTemplateColumns: "42px 1fr", gap: "1.5rem" }}> {/* Dot */} <div style={{ width: 42, height: 42, borderRadius: "var(--radius-full)", background: "var(--color-accent-subtle)", border: "2px solid var(--color-accent)" }}> <Circle style={{ width: 10, height: 10, fill: "var(--color-accent)" }} /> </div> {/* Content */} <div>...</div> </div> ))} </div> </div>Animar cada elemento con un whileInView escalonado
Envuelve cada elemento de la timeline en un motion.div. Pon initial en opacity 0 y x -16, whileInView en opacity 1 y x 0. Pasa viewport once:true para evitar repeticiones. El retardo es i * 0.08, creando una cascada visible al entrar la lista en el viewport sin necesidad de staggerChildren de Framer Motion.
const EASE = [0.16, 1, 0.3, 1] as const; <motion.div initial={{ opacity: 0, x: -16 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ duration: 0.45, delay: i * 0.08, ease: EASE }} >Estilizar con tokens CSS para compatibilidad con temas
Usa var(--color-background), var(--color-foreground), var(--color-foreground-muted), var(--color-accent), var(--color-accent-subtle) y var(--color-border) en todo el componente. Nunca codifiques valores hex en duro. Esto hace que el componente funcione en los 7 presets de tema sin cambiar una sola prop.
Cuándo usarlo
Esta sección encaja mejor en el medio de una página Sobre nosotros, después de una intro de equipo o misión y antes de los testimonios. Funciona para productos SaaS, agencias y startups que tengan de 4 a 6 hitos significativos que mostrar. Evítala si tu empresa tiene menos de un año y menos de 3 hitos, una timeline escasa se percibe vacía en lugar de humilde. Combínala con about-team-intro arriba y testimonials debajo para lograr el arco narrativo más sólido.
Usado por
- Stripe, Usa una disposición de hitos cronológicos en su página About para construir credibilidad a través de una década de historia de producto.
- Linear, Presenta la historia de la empresa como un relato secuencial con fechas, reforzando el cuidado y la intencionalidad.
- Vercel, Organiza las rondas de financiación y los lanzamientos de producto en una timeline vertical para afianzar la confianza de inversores y desarrolladores.
- Notion, Cuenta la historia de su fundación con hitos fechados por año, dando a la historia del producto un tono humano y editorial.
FAQ
¿Cómo evito que la animación de la timeline se vuelva a reproducir al hacer scroll hacia arriba?
Pasa viewport={{ once: true }} a cada motion.div. Framer Motion marca entonces el elemento como animado tras el primer disparo y no lo vuelve a ejecutar, sin importar cuántas veces el usuario pase por delante con el scroll.
¿Puedo controlar la velocidad del escalonado entre los elementos de la timeline?
Sí. El retardo es i * 0.08, cambia el multiplicador. Un valor de 0,05 acelera la cascada, 0,12 la ralentiza. Para timelines muy largas (8+ elementos), mantén el multiplicador por debajo de 0,07 para que los últimos elementos no tarden demasiado en aparecer.
¿Cómo cambio el icono del punto por un símbolo personalizado para cada hito?
Añade un campo icon opcional a la interface TimelineEvent y renderízalo dentro del contenedor del punto. Si el campo está ausente, recurre al icono Circle por defecto. Mantén el tamaño del contenedor exterior del punto fijo en 42px para que la línea de conexión siempre quede alineada.
¿Es este componente accesible para los lectores de pantalla?
La línea de conexión decorativa lleva aria-hidden para que los lectores de pantalla la omitan. Las etiquetas de año son spans de texto plano, los títulos son elementos h3 bajo el h2 de la sección, y todo el contenido está en el flujo del documento, sin trucos solo de CSS que oculten texto.