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

Créer une section blog en timeline verticale avec React

Une section blog en timeline verticale React dispose les cartes d'articles en alternance gauche et droite d'une ligne centrale, chacune glissant depuis son côté au scroll via whileInView de Framer Motion. Le layout repose sur un CSS Grid à trois colonnes (carte / connecteur / carte), la colonne centrale portant le point et la ligne.

  • Stack : React + Framer Motion + lucide-react, environ 97 lignes, aucune dépendance supplémentaire.
  • Moteur de layout : CSS Grid inline à trois colonnes, 1fr / 40px / 1fr, pour aligner les cartes et le connecteur central.
  • Entrée au scroll : whileInView se déclenche une seule fois par carte, avec un décalage de 80 ms entre items et une marge viewport de -50px.
  • Accessible : la ligne centrale est aria-hidden ; les images ont un alt vide (décoratives). Le composant est responsive et testé sur les thèmes clair et sombre.
  • Le premier point de la timeline utilise la couleur d'accent comme ancre visuelle ; les points suivants utilisent la couleur de bordure pour réduire le bruit visuel.

Blog Timeline est une section React qui affiche une liste d'articles le long d'une ligne centrale verticale, avec des cartes alternant gauche et droite à chaque ligne. L'entrée décalée au scroll donne au contenu éditorial un rythme naturel sans nécessiter de bibliothèques complexes. Elle s'adapte aussi bien aux sites d'agence, aux publications média et aux pages de changelog.

Anatomie

La section repose sur un CSS Grid à trois colonnes : une grande colonne de contenu de chaque côté et une colonne centrale de 40px pour le connecteur. Une ligne verticale de 2px traverse toute la hauteur du conteneur, positionnée en absolu à 50%. Chaque ligne place sa carte d'article dans la colonne 1 ou 3 selon son index, tandis que la colonne 2 contient un point de 14px centré au-dessus de la carte. Le premier point utilise la couleur d'accent ; les suivants reprennent le token de bordure. Chaque carte contient une image 16:9, un label de catégorie, un titre, un extrait, un badge de date et une icône flèche.

Comment ça marche

Chaque ligne d'article est une motion.div avec un état initial `opacity: 0` et `x: -30` (cartes gauches) ou `x: 30` (cartes droites). Quand l'élément entre dans le viewport, whileInView le pilote vers `opacity: 1, x: 0`. La transition utilise un easing cubic-bezier personnalisé `[0.16, 1, 0.3, 1]` avec une durée de 600ms, et le délai est calculé comme `index * 0.08` secondes pour créer le décalage. Le flag `once: true` garantit que l'animation ne se joue qu'une seule fois par carte, sans se redéclencher au scroll retour.

Comment le coder en React

  1. Mettre en place le conteneur en grille trois colonnes

    Enveloppe chaque ligne d'article dans un div avec `display: grid` et `gridTemplateColumns: '1fr 40px 1fr'`. Place une ligne verticale de 2px en absolu à 50% du conteneur pour qu'elle passe derrière toutes les lignes. Cette seule définition de grille gère le placement des cartes côté gauche et côté droit.

    <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. Alterner les côtés avec l'index

    Dérive un booléen `isLeft` depuis `i % 2 === 0`. Place la carte dans `gridColumn: isLeft ? '1' : '3'` et l'espaceur vide dans la colonne opposée. Le point se trouve toujours dans la colonne 2. Cela maintient un ordre de markup cohérent quelle que soit la position de la carte.

    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. Ajouter l'entrée décalée au scroll

    Convertis chaque div de ligne en motion.div et règle le décalage x initial selon isLeft. Le délai multiplié par l'index crée l'effet cascade. Garde une marge viewport de -50px pour que l'animation démarre avant que l'élément soit totalement visible, donnant à la page un sentiment de flux continu.

    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. Construire la carte d'article

    Chaque carte est une balise ancre avec une image 16:9 en haut, suivie d'une zone de contenu avec padding. Le label de catégorie utilise le token de couleur d'accent avec uppercase et letter-spacing. La date et la flèche se trouvent dans une flex row en bas. Garde la carte comme un seul `<a>` pour que toute la surface soit cliquable sans éléments interactifs imbriqués.

Quand l'utiliser

Utilise le layout timeline quand tu veux présenter une série d'articles avec un sens de chronologie ou de progression narrative : pages d'index blog, changelogs produit, listings d'études de cas. Évite-le pour les catalogues plats où tous les items ont le même poids et où la comparaison prime sur la séquence. Sur mobile, les colonnes alternées se replier en une seule colonne, donc teste le layout empilé avant de mettre en production.

Utilisé par

  • Stripe, Le blog Stripe utilise un layout éditorial chronologique avec des structures de cartes cohérentes qui mettent en avant la date et la catégorie avant le titre.
  • Vercel, Le changelog et le blog de Vercel listent les articles avec les tags de catégorie et les dates bien visibles, ce qui correspond à l'anatomie de carte d'une section timeline.
  • Linear, La page changelog de Linear est une timeline verticale avec des blocs de contenu alternés, chacun ancré à une date et connecté par une ligne centrale.
  • Framer, Le flux d'updates de Framer organise les notes de version en timeline chronologique avec des cartes et des marqueurs de date sur un axe vertical.

FAQ

Comment rendre la timeline responsive sur mobile ?

Passe d'un grid trois colonnes à un layout mono-colonne sous ton breakpoint. Place chaque carte dans la colonne 1, masque l'espaceur et déplace le point dans une ligne au-dessus de la carte ou convertis-le en bordure gauche colorée. La ligne verticale centrale peut être remplacée par une bordure gauche de 2px sur le wrapper de carte.

Puis-je remplacer whileInView par un IntersectionObserver direct ?

Oui, mais whileInView est plus simple ici car il gère la mise en place de l'observer, le nettoyage et le flag once en interne. Si tu veux éviter Framer Motion, crée une instance d'observer, attache-la à tous les refs de carte en boucle et bascule une classe CSS qui pilote la transition.

Pourquoi le point central a-t-il une couleur différente pour le premier item ?

Le premier point en couleur d'accent marque l'entrée la plus récente comme ancre visuelle de la timeline. Les points suivants utilisent la couleur de bordure pour ne pas concurrencer le contenu des cartes. Tu peux inverser cette logique pour les timelines en ordre ascendant où le dernier item est mis en avant.

L'animation décalée provoque-t-elle un layout shift ?

L'opacité et la transform x sont toutes deux des propriétés gérées exclusivement par le compositor, donc elles s'exécutent sur le GPU sans toucher au layout. Il n'y a aucun impact sur le CLS. Réserve l'espace de chaque carte dans le flux normal dès le départ pour que les dimensions du grid soient stables avant que les animations se déclenchent.

"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

Avis

Section blog timeline verticale React, Code + Tutoriel