How to build a company history timeline in React
A company timeline in React renders a vertical center line with alternating milestone cards that fade up as they enter the viewport, using Framer Motion's whileInView and a staggered delay based on the item index. On mobile the layout collapses to a left-aligned single column.
- Stack: React + Framer Motion + Tailwind v4, ~128 lines, zero extra dependencies.
- Animation: whileInView fade-up (opacity 0→1, y 24→0) with staggered delay of 80ms per item.
- Responsive: full alternating two-column layout on md+, single left-aligned column on mobile.
- Accessible: the decorative vertical line and dot use aria-hidden; semantic h2/h3 heading hierarchy.
- Themeable via CSS custom properties, colors never hardcoded.
About Timeline is a vertically stacked milestone section that visualizes a company's history or product journey. Each milestone fades up as it scrolls into view, alternating left and right on desktop to break the monotony of a flat list. It pairs naturally with a team or founders section lower on the page.
Anatomy
The section centers a max-width-4xl container with a header block (optional badge, h2 title, subtitle paragraph) then the timeline body. The timeline body is a relative div: a 1px vertical rule runs its full height (absolutely positioned at left-4 on mobile, left-1/2 on desktop). Inside, milestone rows alternate via an isLeft flag (even index = left card, odd = right card). Each row contains an accent dot centered on the rule and a text card capped at 50% width on desktop.
How it works
Every milestone card is a motion.div with initial={{ opacity: 0, y: 24 }} and whileInView={{ opacity: 1, y: 0 }}. The viewport prop uses once: true so the animation only fires once, and margin: '-60px' means the card starts animating just before it is fully visible. The transition delay is i * 0.08 seconds, so items entering the viewport at the same time still read in sequence. The header block uses a slightly simpler variant (y: 20, no delay) to settle before the timeline body enters.
How to build it in React
Define your milestone data shape
Start with a typed interface for each milestone: a year string, a title, and a description. Keep the array in mock.ts so the component stays purely presentational. Pass it as a required prop named milestones.
interface MilestoneItem { year: string; title: string; description: string; }Draw the vertical rule
Wrap the milestone list in a relative div, then place an absolutely-positioned 1px div that spans top-0 to bottom-0. Use left-4 by default and md:left-1/2 so it shifts to the center on desktop. Mark it aria-hidden so screen readers skip it.
<div className="absolute left-4 top-0 bottom-0 w-px md:left-1/2" style={{ backgroundColor: "var(--color-border)" }} aria-hidden />Alternate the cards and add the dot
For each milestone, compute isLeft = i % 2 === 0. The card div gets md:w-[calc(50%-2rem)] plus either md:mr-auto md:text-right md:pr-8 (left side) or md:ml-auto md:pl-8 (right side). The dot is an absolutely-positioned circle at left-2 on mobile, md:left-1/2 md:-translate-x-1/2 on desktop, layered above the rule with z-10.
const isLeft = i % 2 === 0; // card className={`md:w-[calc(50%-2rem)] ${ isLeft ? "md:mr-auto md:text-right md:pr-8" : "md:ml-auto md:pl-8" }`}Wire up the scroll animations
Wrap each milestone row in a motion.div. Set initial to opacity 0 and y 24, whileInView to opacity 1 and y 0, then pass viewport={{ once: true, margin: '-60px' }}. Compute the delay as i * 0.08 so items stagger naturally even when they enter the viewport together.
<motion.div initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ delay: i * 0.08, duration: 0.5, ease: [0.16, 1, 0.3, 1] }} >
When to use it
Reach for it on about pages, brand story sections, or product launch narratives where you want to convey growth over time. It works for any company with four or more milestones; fewer than that and a simple bullet list reads faster. Avoid it on conversion-focused pages where the scroll journey competes with a CTA, and skip the alternating layout on data-heavy timelines where readability matters more than visual rhythm.
Used by
FAQ
How do I control the stagger speed between milestones?
Adjust the multiplier in the delay calculation: delay: i * 0.08. A smaller value like 0.05 tightens the sequence; 0.12 spreads it out more. If milestones enter the viewport one at a time because of the page length, the delay barely matters, only once: true and the margin offset count.
Can I make the vertical line animate as the user scrolls?
Yes. Replace the static div with a motion.div and animate its scaleY from 0 to 1 using useScroll and useTransform, with transformOrigin set to top. This creates a line that draws itself as the user scrolls down the section.
Does the alternating layout break on very narrow screens?
No, below the md breakpoint the layout switches to a single left-aligned column (pl-12 on each row, with the dot and rule pinned to the left at left-4). The alternating classes only apply from md upward.
How many milestones is too many?
Past twelve items the section becomes exhausting to scroll. If you have more data, consider grouping by decade or using a horizontal scrollable timeline instead. Six to eight milestones is a comfortable range for a single About page section.