Retour au catalogue

Blog Timeline

Articles de blog sur une timeline verticale, alternant gauche/droite avec une ligne de connexion.

blogmedium Both Responsive a11y
editorialelegantuniversalagencyeducationstacked
Theme

How to build a vertical timeline blog section in React

A vertical timeline blog section in React places article cards alternating left and right of a centered vertical line, each sliding in from its side on scroll using Framer Motion's whileInView. The layout uses a three-column CSS Grid (card / connector / card) with the center column holding the dot and line.

  • Stack: React + Framer Motion + lucide-react, approximately 97 lines, no additional dependencies.
  • Layout engine: inline CSS Grid with three columns, 1fr / 40px / 1fr, to align cards and the center connector.
  • Scroll entrance: whileInView triggers once per card, with an 80ms stagger between items and a viewport margin of -50px.
  • Accessible: the center line is aria-hidden; images use empty alt text (decorative). The component is responsive and tested on both light and dark themes.
  • The first timeline dot uses the accent color as a visual anchor; subsequent dots use the border color to reduce visual noise.

Blog Timeline is a React section that renders a list of articles along a vertical center line, with cards alternating left and right on each row. The staggered scroll-triggered entrance gives editorial content a sense of pace without requiring complex libraries. It fits equally well on agency sites, media publications, and changelog pages.

Anatomy

The section is built around a three-column CSS Grid: a wide content column on each side and a narrow 40px center column for the connector. A 2px vertical line spans the full height of the container, absolutely positioned at 50%. Each row places its article card in either column 1 or column 3 depending on its index, while column 2 holds a 14px dot centered above the card. The first dot is accent-colored; the rest match the border token. Each card contains a 16:9 image, a category label, a title, an excerpt, a date badge, and an arrow icon.

How it works

Each article row is a motion.div with an initial state of `opacity: 0` and `x: -30` (left cards) or `x: 30` (right cards). When the element enters the viewport, whileInView drives it to `opacity: 1, x: 0`. The transition uses a custom cubic-bezier ease `[0.16, 1, 0.3, 1]` with a duration of 600ms, and the delay is computed as `index * 0.08` seconds to create the stagger. The `once: true` flag ensures the animation plays a single time per card, avoiding re-triggering on scroll back.

How to build it in React

  1. Set up the three-column grid container

    Wrap each article row in a div with `display: grid` and `gridTemplateColumns: '1fr 40px 1fr'`. Place a 2px vertical line absolutely at 50% of the container to run behind all rows. This single grid definition handles both left-side and right-side card placement.

    <div style={{ position: "relative", maxWidth: "900px", margin: "0 auto" }}>
      {/* Center line */}
      <div aria-hidden style={{ position: "absolute", left: "50%", top: 0, bottom: 0, width: 2 }} />
      {articles.map((article, i) => (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 40px 1fr" }} key={i}>
          {/* card + dot + spacer */}
        </div>
      ))}
    </div>
  2. Alternate card sides with the index

    Derive a boolean `isLeft` from `i % 2 === 0`. Place the card in `gridColumn: isLeft ? '1' : '3'` and the empty spacer in the opposite column. The dot always sits in column 2. This keeps the markup order consistent regardless of which side the card appears on.

    const isLeft = i % 2 === 0;
    // Card
    <div style={{ gridColumn: isLeft ? "1" : "3" }}>...</div>
    // Dot
    <div style={{ gridColumn: "2", display: "flex", justifyContent: "center" }}>
      <div style={{ width: 14, height: 14, borderRadius: "50%", background: i === 0 ? "var(--color-accent)" : "var(--color-border)" }} />
    </div>
    // Spacer
    <div style={{ gridColumn: isLeft ? "3" : "1" }} />
  3. Add the staggered scroll entrance

    Convert each row div to a motion.div and set the initial x offset based on isLeft. The delay multiplied by the index creates the cascade. Keep viewport margin at -50px so the animation starts before the element fully enters the screen, giving the page a sense of continuous flow.

    const EASE = [0.16, 1, 0.3, 1] as const;
    
    <motion.div
      initial={{ opacity: 0, x: isLeft ? -30 : 30 }}
      whileInView={{ opacity: 1, x: 0 }}
      viewport={{ once: true, margin: "-50px" }}
      transition={{ duration: 0.6, delay: i * 0.08, ease: EASE }}
      style={{ display: "grid", gridTemplateColumns: "1fr 40px 1fr" }}
    >
  4. Build the article card

    Each card is a plain anchor tag with a 16:9 image at the top, followed by a padded content area. The category label uses the accent color token with uppercase letter-spacing. Date and arrow sit in a flex row at the bottom. Keep the card as a single `<a>` so the entire surface is clickable without nested interactive elements.

When to use it

Reach for the timeline layout when you want to present a series of articles with a sense of chronology or narrative progression, blog index pages, product changelogs, case study listings. Skip it for flat catalog pages where all items carry equal weight and comparison matters more than sequence. On mobile the alternating columns collapse to a single column, so test the stacked layout before shipping.

Used by

  • Stripe, Stripe's blog uses a chronological editorial layout with consistent card structures that emphasize date and category before the headline.
  • Vercel, Vercel's changelog and blog list articles with category tags and dates front and center, matching the card anatomy of a timeline section.
  • Linear, Linear's changelog page is a vertical timeline with alternating content blocks, each anchored to a date and connected by a center line.
  • Framer, Framer's updates feed arranges release notes in a chronological timeline layout with cards and date markers on a vertical axis.

FAQ

How do I make the timeline responsive on mobile?

Switch from a three-column grid to a single-column layout below your breakpoint. Place every card in column 1, hide the spacer, and move the dot to a row above the card or convert it to a left-border accent. The center vertical line can be replaced by a left-aligned 2px border on the card wrapper.

Can I replace whileInView with an IntersectionObserver directly?

Yes, but whileInView is simpler here because it handles the observer setup, cleanup, and the once flag internally. If you need to avoid Framer Motion entirely, create one observer instance, attach it to all card refs in a loop, and toggle a CSS class that drives the transition.

Why is the center dot a different color for the first item?

The accent-colored first dot marks the most recent entry as the visual anchor of the timeline. Subsequent dots use the border color to avoid competing with the card content for attention. You can reverse this logic for ascending-order timelines where the last item is the highlight.

Does the stagger animation cause layout shift?

The opacity and x transform are both compositor-only properties, so they run on the GPU without touching layout. There is no CLS impact. Reserve space for each card in the normal flow from the start so the grid dimensions are stable before any animation fires.

"use client";

import { motion } from "framer-motion";
import { Calendar, ArrowUpRight } from "lucide-react";

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

interface BlogTimelineProps {
  sectionTitle?: string;
  sectionSubtitle?: string;
  articles?: Article[];
}

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

export default function BlogTimeline({

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reviews

React Vertical Timeline Blog Section, Code + Tutorial