# About Story Timeline — React Team Section

> Company milestones alternate either side of a centre line that fills as you scroll; each year lights its node and slides its card in from its own side.

- Category: [Team & About](https://pixelpapa.com/components/team.md)
- Tags: about, timeline, story, scroll, milestones
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/team/about-story-timeline

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/about-story-timeline.json"
```

The file is written to `components/pixelpapa/about-story-timeline.tsx`. To install manually, run `npm install motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { motion, useReducedMotion, useScroll, useSpring } from "motion/react";
import { useRef } from "react";
import { cn } from "@/lib/utils";

type Milestone = { year: string; title: string; body: string; stat?: [string, string] };

const defaultMilestones: Milestone[] = [
  {
    year: "2018",
    title: "A spreadsheet and a van",
    body: "Jonas and Mireille start delivering surplus bakery bread to shelters in Lyon, tracking every loaf in a shared sheet.",
    stat: ["1", "van, borrowed"],
  },
  {
    year: "2020",
    title: "The app nobody asked for",
    body: "Lockdown triples demand overnight. We build the first Crumb app in eleven days so bakeries can post leftovers themselves.",
    stat: ["140", "bakeries in 3 months"],
  },
  {
    year: "2022",
    title: "Beyond bread",
    body: "Grocers and canteens join. We raise a €4.2M seed round and hire our first engineer who is not a co-founder.",
    stat: ["€4.2M", "seed round"],
  },
  {
    year: "2024",
    title: "Six countries, one route planner",
    body: "Our matching engine learns to chain pickups, cutting the average delivery run from 74 to 31 minutes.",
    stat: ["31 min", "average run"],
  },
  {
    year: "2026",
    title: "Ten million meals",
    body: "The ten-millionth meal is a tray of pains aux raisins from the same bakery that gave us our first loaf.",
    stat: ["10M", "meals rescued"],
  },
];

export default function AboutStoryTimeline({ milestones = defaultMilestones }: { milestones?: Milestone[] }) {
  const track = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();
  const { scrollYProgress } = useScroll({ target: track, offset: ["start 0.75", "end 0.55"] });
  const fill = useSpring(scrollYProgress, { stiffness: 90, damping: 24, restDelta: 0.001 });

  return (
    <section className="w-full bg-background px-6 py-20">
      <div className="mx-auto max-w-5xl">
        <div className="mx-auto max-w-2xl text-center">
          <p className="text-sm font-medium text-primary">Our story</p>
          <h2 className="mt-3 text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl">
            Eight years of moving good food <span className="font-serif font-normal italic">before</span> it becomes waste.
          </h2>
        </div>

        <div ref={track} className="relative mt-16">
          {/* Rail and its scroll-driven fill. */}
          <div aria-hidden className="absolute top-0 bottom-0 left-4 w-px bg-border md:left-1/2" />
          <motion.div
            aria-hidden
            style={{ scaleY: reduce ? 1 : fill }}
            className="absolute top-0 bottom-0 left-4 w-px origin-top bg-primary md:left-1/2"
          />
          <ol>

          {milestones.map((m, i) => {
            const right = i % 2 === 1;
            return (
              <motion.li
                key={m.year}
                initial="hidden"
                whileInView="shown"
                viewport={{ once: true, margin: "0px 0px -18% 0px" }}
                className="relative grid gap-x-16 pb-14 pl-12 last:pb-0 md:grid-cols-2 md:pl-0"
              >
                <motion.span
                  aria-hidden
                  variants={{
                    hidden: { scale: 0.6, backgroundColor: "var(--background)", borderColor: "var(--border)" },
                    shown: { scale: 1, backgroundColor: "var(--primary)", borderColor: "var(--primary)" },
                  }}
                  transition={{ type: "spring", stiffness: 300, damping: 18 }}
                  className="absolute top-2 left-4 z-10 size-3.5 -translate-x-1/2 rounded-full border-2 ring-4 ring-background md:left-1/2"
                />
                <motion.div
                  variants={{
                    hidden: reduce ? { opacity: 0 } : { opacity: 0, x: right ? 40 : -40, filter: "blur(6px)" },
                    shown: { opacity: 1, x: 0, filter: "blur(0px)" },
                  }}
                  transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
                  className={cn(right ? "md:col-start-2" : "md:text-right")}
                >
                  <p className="font-serif text-6xl leading-none tracking-tight text-foreground/15 sm:text-7xl">{m.year}</p>
                  <h3 className="mt-3 text-xl font-semibold tracking-tight text-foreground">{m.title}</h3>
                  <p className={cn("mt-2 max-w-sm text-muted-foreground", !right && "md:ml-auto")}>{m.body}</p>
                  {m.stat && (
                    <p className="mt-4 inline-flex items-baseline gap-2 rounded-full border border-border bg-card px-3.5 py-1.5 text-sm">
                      <span className="font-semibold text-primary tabular-nums">{m.stat[0]}</span>
                      <span className="text-muted-foreground">{m.stat[1]}</span>
                    </p>
                  )}
                </motion.div>
              </motion.li>
            );
          })}
          </ol>
        </div>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "About Story Timeline" component from PixelPapa UI to this project.

What it is: Company milestones alternate either side of a centre line that fills as you scroll; each year lights its node and slides its card in from its own side.
Category: Team & About. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/about-story-timeline.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/about-story-timeline.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/about-story-timeline" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More team & about

- [Team Grid Hover Bio](https://pixelpapa.com/components/team/team-grid-hover-bio.md): A grid of drawn gradient portraits; hovering or focusing a person slides a frosted bio panel up over the portrait while the silhouette drifts back.
- [Team Leadership Rows](https://pixelpapa.com/components/team/team-leadership-rows.md): Leadership as large typographic rows between hairlines; the hovered or focused row springs open to show a portrait, a quote and a few facts.
- [Team Values And Photos](https://pixelpapa.com/components/team/team-values-and-photos.md) (Pro): A numbered values list that advances on a timer beside a collage of gradient photo tiles; the tiles tied to the active value lift forward while the rest recede.
