Retour au catalogue

About Timeline Interactive

Timeline avec points cliquables et ligne de progression animee. Au clic, le contenu de droite change avec AnimatePresence slide. Layout split.

aboutcomplex Both Responsive a11y
corporateelegantuniversalagencysaassplit
Theme

Créer une section timeline interactive cliquable en React

Une timeline interactive en React affiche une liste verticale de boutons d'années cliquables à gauche et un panneau de détail animé à droite. Une ligne pilotée par un spring progresse au fil des événements ; AnimatePresence bascule le contenu du panneau avec un slide horizontal à chaque clic.

  • Stack : React + Framer Motion 11 + CSS custom properties, ~78 lignes, zéro dépendance supplémentaire.
  • État : un seul index `active` pilote à la fois la hauteur de la ligne de progression et le contenu du panneau.
  • Animation : `type: spring` pour la ligne, `AnimatePresence mode='wait'` pour le slide du panneau.
  • Accessible : boutons avec onClick, navigable au clavier ; le point actif grossit pour indiquer la sélection.
  • Supporte 3 à 6 événements ; en dessous de 3, l'effet de la ligne de progression perd son impact narratif.

Ce composant transforme l'historique d'une entreprise en récit interactif. La colonne de gauche liste les étapes clés sous forme de boutons ; cliquer sur l'une d'elles fait progresser une ligne accent animée en spring jusqu'à ce point et fait glisser une carte de détail dans le panneau de droite. Il remplace une liste statique par un rythme que le visiteur maîtrise.

Anatomie

Le layout est une grille CSS deux colonnes (1fr / 2fr). La colonne de gauche contient un conteneur relatif avec une ligne grise statique et une ligne accent animée en spring superposée, toutes deux positionnées en absolu. Chaque événement s'affiche comme un bouton natif contenant un motion.div dot (le sélecteur) et le texte de l'année et du titre. La colonne de droite est une carte à min-height fixe qui accueille le panneau AnimatePresence ; un grand chiffre d'année semi-transparent trône en haut comme filigrane décoratif.

Comment ça marche

La hauteur de la ligne est calculée en `(active / Math.max(events.length - 1, 1)) * 100` pourcent, animée avec `type: 'spring', stiffness: 80, damping: 20` pour une transition douce au clic. Le fond et l'échelle de chaque point basculent selon que son index est inférieur ou égal à `active`, donnant une lecture 'rempli jusqu'ici'. À droite, `AnimatePresence mode='wait'` enveloppe un `motion.div` dont la clé est `active` ; il entre depuis `x: 30` et sort vers `x: -30`, ce qui donne l'impression de faire défiler le temps vers l'avant.

Comment le coder en React

  1. Mettre en place la grille et la ligne verticale

    Crée un conteneur grid deux colonnes. Dans la colonne gauche, ajoute un wrapper relatif et positionne en absolu une ligne grise (hauteur complète) et un motion.div accent (hauteur variable). Les deux partagent les mêmes valeurs `left`, `top` et `width` ; seule la ligne accent reçoit `animate={{ height }}` piloté par l'index actif.

    const lineBase = { position: "absolute", left: 7, top: 4, width: 2, borderRadius: 1 };
    
    // Static track
    <div style={{ ...lineBase, bottom: 4, background: "var(--color-border)" }} />
    
    // Animated fill
    <motion.div
      animate={{ height: `${(active / Math.max(events.length - 1, 1)) * 100}%` }}
      transition={{ type: "spring", stiffness: 80, damping: 20 }}
      style={{ ...lineBase, background: "var(--color-accent)" }}
    />
  2. Afficher les boutons d'événements avec des points animés

    Itère sur les événements et rends un bouton natif pour chacun. À l'intérieur, place un motion.div dot positionné en absolu pour s'aligner sur la ligne verticale. Anime son `scale` (1 ou 1.3) et son `background` (accent si inférieur ou égal à active, couleur border sinon) pour que l'état rempli/non-rempli soit immédiatement lisible.

    <motion.div
      animate={{
        scale: i === active ? 1.3 : 1,
        background: i <= active ? "var(--color-accent)" : "var(--color-border)",
      }}
      transition={{ type: "spring", stiffness: 300, damping: 20 }}
      style={{ position: "absolute", left: "-2rem", width: 14, height: 14, borderRadius: "50%" }}
    />
  3. Basculer le panneau de détail avec AnimatePresence

    Dans la colonne de droite, enveloppe un motion.div dans AnimatePresence avec `mode='wait'`. Donne au motion.div la clé `active` pour que React démonte l'ancien contenu avant de monter le nouveau. Utilise `initial={{ opacity: 0, x: 30 }}`, `animate={{ opacity: 1, x: 0 }}`, `exit={{ opacity: 0, x: -30 }}` pour l'effet de défilement vers l'avant.

    <AnimatePresence mode="wait">
      {current && (
        <motion.div
          key={active}
          initial={{ opacity: 0, x: 30 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: -30 }}
          transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
        >
          <span style={{ fontSize: "3rem", fontWeight: 900, opacity: 0.2 }}>{current.year}</span>
          <h3>{current.title}</h3>
          <p>{current.description}</p>
        </motion.div>
      )}
    </AnimatePresence>
  4. Connecter l'état et passer les données

    Un seul index `useState(0)` suffit pour tout l'état. Passe un tableau `events` (year, title, description) en prop avec des données par défaut. Sur mobile, la grille deux colonnes passe en colonne unique via une media query ou un fallback grid responsive ; la ligne reste fonctionnelle mais le split disparaît.

Quand l'utiliser

Ce pattern convient aux pages About d'entreprises, d'agences ou de startups qui ont une chronologie significative : levées de fonds, lancements de produits, ouvertures de bureaux, pivots. Il fonctionne mieux avec 4 à 6 étapes où chaque événement a une vraie histoire à raconter. À éviter si votre historique comporte moins de 3 événements (la ligne animée perd son effet) ou si la page est déjà dense avec des sections en concurrence. Sur mobile le layout split se replie en colonne unique ; vérifiez que le rendu mono-colonne paraît intentionnel.

Utilisé par

  • Stripe, Utilise un récit basé sur les étapes clés sur sa page About pour raconter la croissance de l'entreprise depuis 2010.
  • Linear, Présente l'historique de l'entreprise comme un récit progressif où chaque chapitre est une étape distincte, proche de cette approche en panneau split.
  • Notion, Page About interactive avec des jalons ancrés par année et des panneaux de contenu extensibles pour chaque période de l'entreprise.

FAQ

Combien d'événements sont recommandés ?

Entre 4 et 6. En dessous de 3, l'animation de la ligne de progression paraît inutile car il n'y a presque rien à remplir. Au-delà de 6, la colonne de gauche devient surchargée et les points sont difficiles à toucher sur mobile.

Peut-on faire défiler les événements automatiquement ?

Oui. Ajoute un useEffect avec un setInterval qui incrémente `active` jusqu'à `events.length - 1`, puis nettoie à l'unmount. Mets en pause au survol en effaçant l'intervalle dans onMouseEnter et en le relançant dans onMouseLeave.

Pourquoi la ligne utilise-t-elle une hauteur en pourcentage plutôt qu'en pixels ?

Le pourcentage rend le calcul indépendant de la hauteur réelle de la colonne gauche, qui varie selon le contenu. La formule `active / (events.length - 1) * 100%` atteint toujours exactement le dernier point, quel que soit le nombre d'événements ou la hauteur du texte.

Le layout fonctionne-t-il sur mobile ?

Le composant est marqué responsive dans ses métadonnées, mais la grille 1fr/2fr nécessite un breakpoint explicite pour passer en colonne unique. Ajoute une media query CSS ou bascule sur `gridTemplateColumns: '1fr'` en dessous de 640px. La logique de la timeline et les animations fonctionnent sans modification en mode colonne unique.

"use client";

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

interface TimelineEvent { year: string; title: string; description: string }
interface AboutTimelineInteractiveProps { title?: string; subtitle?: string; events?: TimelineEvent[] }

const EASE = [0.16, 1, 0.3, 1] as const;
const lineBase = { position: "absolute" as const, left: 7, top: 4, width: 2, borderRadius: 1 };

export default function AboutTimelineInteractive({
  title = "Notre parcours",
  subtitle = "Les etapes cles qui ont forge notre identite.",
  events = [],
}: AboutTimelineInteractiveProps) {
  const [active, setActive] = useState(0);
  const current = events[active];

  return (
    <section style={{ padding: "var(--section-padding-y) 0", background: "var(--color-background)" }}>
      <div style={{ maxWidth: "var(--container-max-width)", margin: "0 auto", padding: "0 var(--container-padding-x)" }}>

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Timeline interactive React avec Framer Motion, Tutoriel