Créer une section À propos avec timeline verticale en React
Une section À propos avec timeline verticale en React affiche chaque jalon comme une ligne de grille (point + contenu), décalée grâce au whileInView de Framer Motion pour que les éléments entrent en fondu et glissent au scroll. Un seul div positionné en absolu trace le trait vertical derrière tous les points.
- Stack : React + Framer Motion + lucide-react, ~160 lignes, zéro dépendance supplémentaire.
- Animation : whileInView avec once:true, entrée en glissement sur l'axe x, délai décalé de 0,08s × index.
- Thème : 100% CSS custom properties, background, foreground, accent, border, radius. Aucune couleur codée en dur.
- Accessible : le trait de timeline est aria-hidden, hiérarchie sémantique h2/h3, lisible en mode fort contraste.
- Responsive par défaut, clamp() pour la taille du titre, grille mono-colonne adaptée à tout viewport.
About Story Narrative est une section React qui raconte l'histoire d'une entreprise sous forme de timeline verticale. Chaque jalon glisse depuis la gauche au moment où il entre dans le viewport, piloté par le whileInView de Framer Motion. Le rendu est éditorial et soigné, adapté aux pages À propos d'agences, de SaaS ou de startups où la crédibilité se construit jalon après jalon.
Anatomie
La section comporte deux zones. En haut, un bloc d'en-tête centré (h2 + paragraphe d'introduction) qui monte en fondu une seule fois via un motion.div. En dessous, un conteneur relative contient le trait vertical, un div de 2px positionné en absolu de haut en bas sur le bord gauche, et une colonne flex d'éléments de timeline. Chaque élément est une grille 2 colonnes : un point d'accent de 42px à gauche et un bloc texte à droite (étiquette d'année en couleur d'accent, titre h3, paragraphe).
Comment ça marche
Chaque jalon est enveloppé dans un motion.div avec initial={{ opacity: 0, x: -16 }} et whileInView={{ opacity: 1, x: 0 }}. L'option viewport once:true garantit que l'animation se déclenche une seule fois et reste en place, même pour les scrolleurs rapides. Le délai est calculé comme i * 0.08, produisant un décalage naturel sans boilerplate d'orchestration. Le bloc d'en-tête utilise y: 16 au lieu de x: -16 pour une distinction visuelle. Les deux partagent le même EASE cubique [0.16, 1, 0.3, 1] pour un ressenti vif et légèrement rebondissant.
Comment le coder en React
Définir la structure des données et afficher l'en-tête
Crée une interface TimelineEvent avec les champs year, title et description. Accepte sectionTitle, intro et timeline comme props avec des valeurs par défaut. Enveloppe le h2 + paragraphe dans un motion.div qui entre en fondu depuis y:16 avec viewport once:true. Utilise clamp() pour le font-size du titre afin qu'il s'adapte sans 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>Tracer le trait vertical et le point
Dans un conteneur relative, place un div de 2px de large positionné en absolu sur toute la hauteur du côté gauche. C'est le trait de connexion. Chaque élément de timeline est une grille CSS 2 colonnes : 42px pour la colonne du point, 1fr pour le contenu. Le point est un cercle flex avec une bordure et un remplissage accent via l'icône Circle de lucide-react à 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>Animer chaque élément avec un décalage whileInView
Enveloppe chaque élément de la timeline dans un motion.div. Mets initial à opacity 0 et x -16, whileInView à opacity 1 et x 0. Passe viewport once:true pour éviter les répétitions. Le délai vaut i * 0.08, créant une cascade visible à l'entrée de la liste dans le viewport sans recourir à 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 }} >Styler avec des tokens CSS pour la compatibilité thème
Utilise var(--color-background), var(--color-foreground), var(--color-foreground-muted), var(--color-accent), var(--color-accent-subtle) et var(--color-border) partout. Ne jamais coder de valeurs hex en dur. Cela rend le composant compatible avec les 7 presets de thème sans modifier une seule prop.
Quand l'utiliser
Cette section s'intègre au mieux au milieu d'une page À propos, après une intro équipe ou mission et avant les témoignages. Elle convient aux produits SaaS, agences et startups qui ont 4 à 6 jalons significatifs à montrer. À éviter si l'entreprise a moins d'un an avec moins de 3 jalons, une timeline clairsemée paraît vide plutôt que modeste. Associe-la à about-team-intro au-dessus et à des testimonials en dessous pour l'arc narratif le plus fort.
Utilisé par
- Stripe, Utilise une mise en page de jalons chronologiques sur sa page À propos pour asseoir sa crédibilité sur une décennie d'histoire produit.
- Linear, Présente l'histoire de l'entreprise comme un récit séquentiel avec des dates, renforçant la notion de soin et d'intentionnalité.
- Vercel, Organise les levées de fonds et les lancements produit dans une timeline verticale pour ancrer la confiance des investisseurs et des développeurs.
- Notion, Raconte l'histoire de la fondation avec des jalons horodatés à l'année, donnant à l'histoire du produit un ton humain et éditorial.
FAQ
Comment empêcher l'animation de la timeline de se rejouer au scroll vers le haut ?
Passe viewport={{ once: true }} à chaque motion.div. Framer Motion marque alors l'élément comme animé après le premier déclenchement et ne le rejoue plus, peu importe combien de fois l'utilisateur scrolle devant.
Puis-je contrôler la vitesse du décalage entre les éléments de la timeline ?
Oui. Le délai est i * 0.08, change le multiplicateur. Une valeur de 0,05 accélère la cascade, 0,12 la ralentit. Pour les timelines très longues (8+ éléments), garde le multiplicateur en dessous de 0,07 pour que les derniers n'attendent pas trop longtemps avant d'apparaître.
Comment remplacer le point par un symbole personnalisé pour chaque jalon ?
Ajoute un champ icon optionnel à l'interface TimelineEvent et rends-le dans le conteneur du point. Si le champ est absent, replie-toi sur l'icône Circle par défaut. Garde le conteneur du point fixé à 42px pour que le trait de connexion reste toujours aligné.
Ce composant est-il accessible aux lecteurs d'écran ?
Le trait de connexion décoratif porte aria-hidden pour que les lecteurs d'écran l'ignorent. Les étiquettes d'année sont des spans en texte simple, les titres sont des éléments h3 sous le h2 de la section, et tout le contenu est dans le flux du document, aucune astuce CSS qui masque du texte.