Retour au catalogue

About Story Narrative

Histoire de l'entreprise en format narratif avec timeline integree verticale.

aboutmedium Both Responsive a11y
editorialelegantuniversalagencysaascentered
Theme

How to build a vertical timeline about section in React

A vertical timeline about section in React renders each milestone as a grid row (dot + content), staggered with Framer Motion's whileInView so items fade and slide in as the user scrolls. A single absolutely-positioned div draws the connecting line behind all dots.

  • Stack: React + Framer Motion + lucide-react, ~160 lines, zero extra dependencies.
  • Animation: whileInView with once:true, x-slide entry per item, stagger delay of 0.08s × index.
  • Theming: 100% CSS custom properties, background, foreground, accent, border, radius. No hardcoded colors.
  • Accessible: timeline line is aria-hidden, semantic h2/h3 heading hierarchy, readable in high-contrast mode.
  • Responsive by default, clamp() for heading size, mobile-friendly single-column grid at any viewport.

About Story Narrative is a React section that tells a company's history as a vertical timeline. Each milestone slides in from the left as it enters the viewport, driven by Framer Motion's whileInView. The result reads as editorial and intentional, suited for agency, SaaS, and startup about pages where credibility is built milestone by milestone.

Anatomy

The section has two zones. At the top, a centered header block (h2 + intro paragraph) fades up once with a single motion.div. Below it, a relative container holds the vertical line, a 2px div positioned absolute from top to bottom on the left edge, and a flex column of timeline items. Each item is a 2-column grid: a 42px accent dot on the left and a text block on the right (year label in accent color, h3 title, paragraph).

How it works

Every milestone is wrapped in a motion.div with initial={{ opacity: 0, x: -16 }} and whileInView={{ opacity: 1, x: 0 }}. The viewport option once:true means the animation fires once and stays put, so fast scrollers never see a repeat. The delay is computed as i * 0.08, giving a natural stagger without any orchestration boilerplate. The header block uses y: 16 instead of x: -16 for visual distinction. Both use the same custom EASE cubic-bezier [0.16, 1, 0.3, 1] for a snappy, overshooting feel.

How to build it in React

  1. Define the data shape and render the header

    Create a TimelineEvent interface with year, title, and description fields. Accept sectionTitle, intro, and timeline as props with defaults. Wrap the h2 + paragraph in a motion.div that fades in from y:16 with viewport once:true. Use clamp() for the heading font-size so it scales without 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>
  2. Draw the vertical line and dot

    Inside a relative container, place a 2px wide, absolutely-positioned div that spans the full height on the left side. This is the connector line. Each timeline item is a 2-column CSS grid: 42px for the dot column, 1fr for the content. The dot is a flex circle with a border and accent fill using the Circle icon from lucide-react at 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>
  3. Animate each item with staggered whileInView

    Wrap each timeline item in a motion.div. Set initial to opacity 0 and x -16, whileInView to opacity 1 and x 0. Pass viewport once:true to prevent replays. The delay is i * 0.08, creating a visible cascade as the list enters the viewport without needing Framer Motion's staggerChildren.

    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 }}
    >
  4. Style with CSS tokens for theme compatibility

    Use var(--color-background), var(--color-foreground), var(--color-foreground-muted), var(--color-accent), var(--color-accent-subtle), and var(--color-border) throughout. Never hardcode hex values. This makes the component work across all 7 theme presets without a single prop change.

When to use it

This section fits best in the middle of an about page, after a team or mission intro and before testimonials. It works for SaaS products, agencies, and startups that have 4 to 6 meaningful milestones to show. Skip it if your company is under a year old with fewer than 3 milestones, a sparse timeline reads as empty rather than humble. Pair it with about-team-intro above and testimonials below for the strongest narrative arc.

Used by

  • Stripe, Uses a chronological milestone layout on its About page to build credibility through a decade of product history.
  • Linear, Presents the company story as a sequential narrative with dates, reinforcing craft and intentionality.
  • Vercel, Organizes funding rounds and product launches in a vertical timeline to anchor investor and developer trust.
  • Notion, Tells the founding story with year-stamped milestones, giving the product history a human, editorial feel.

FAQ

How do I prevent the timeline animation from replaying on scroll-up?

Pass viewport={{ once: true }} to every motion.div. Framer Motion then marks the element as animated after the first trigger and never fires it again, regardless of how many times the user scrolls past it.

Can I control the stagger speed between timeline items?

Yes. The delay is i * 0.08, change the multiplier. A value of 0.05 speeds up the cascade, 0.12 slows it down. For very long timelines (8+ items), keep the multiplier below 0.07 so the last items don't wait too long to appear.

How do I swap the dot icon for a custom symbol per milestone?

Add an optional icon field to the TimelineEvent interface and render it inside the dot container. If the field is absent, fall back to the default Circle icon. Keep the outer dot container size fixed at 42px so the connector line always aligns.

Is this accessible to screen readers?

The decorative connector line carries aria-hidden so screen readers skip it. The year labels are plain text spans, titles are h3 elements under the section's h2, and all content is in document flow, no CSS-only tricks that hide text.

"use client";

import { motion } from "framer-motion";
import { Circle } from "lucide-react";

interface TimelineEvent {
  year: string;
  title: string;
  description: string;
}

interface AboutStoryNarrativeProps {
  sectionTitle?: string;
  intro?: string;
  timeline?: TimelineEvent[];
}

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

export default function AboutStoryNarrative({
  sectionTitle = "Notre histoire",
  intro = "Comment tout a commence.",

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reviews

React Vertical Timeline About Section, Code + Tutorial