Scroll Effects

Scroll Timeline

A scroll-driven timeline where the growing line extends along the main axis and each node lights up and reveals its content as the line arrives.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add scroll-timeline.en

New project or installation trouble? Start here →

Scroll Timeline is a React scroll-driven timeline component built with Tailwind CSS and Motion's useScroll. A line on the left rail grows as you scroll, each node lights up as the line reaches it and its content fades in, and the markup is a semantic ol list.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.5
Active edited usageDefault props
<ScrollTimeline/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Company history or milestones on an "About us" page.
  • Product roadmaps or changelogs organized by quarter or version.
  • Onboarding flows or application steps that should be read in order.
  • The experience section of a resume or portfolio.

Installation

npx webberui@latest add https://webberui.com/r/scroll-timeline.en.json

Or, once registries are configured in components.json, install it as @webberui/scroll-timeline.

Usage

Wrap a set of ScrollTimelineItem elements in ScrollTimeline. As you scroll, the growing line on the central axis extends from top to bottom and each node lights up as the line reaches it.

import { Flag, Rocket } from "lucide-react";
import {
  ScrollTimeline,
  ScrollTimelineItem,
} from "@/components/ui/scroll-timeline";

<ScrollTimeline>
  <ScrollTimelineItem icon={<Flag />} date="2024 · Q1" title="Project kickoff">
    Settled on an animation-first design principle.
  </ScrollTimelineItem>
  <ScrollTimelineItem icon={<Rocket />} date="2025 · Q1" title="Public launch">
    Scroll to here and the node lights up together with the growing line.
  </ScrollTimelineItem>
</ScrollTimeline>;

If the timeline sits inside a fixed-height nested scroll container, pass that container's ref to container:

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
  <ScrollTimeline container={scrollerRef}>{/* ... */}</ScrollTimeline>
</div>;

Examples

Generate nodes from a data array

Node data usually comes from a CMS or a constants file, so map it into ScrollTimelineItem elements; icon and date are optional.

import {
  ScrollTimeline,
  ScrollTimelineItem,
} from "@/components/ui/scroll-timeline";

const releases = [
  { version: "v1.0", date: "March 2024", note: "First public release with projects and task management." },
  { version: "v1.4", date: "September 2024", note: "Added Gantt charts and CSV export." },
  { version: "v2.0", date: "April 2025", note: "Rebuilt permissions with custom roles." },
];

export function Changelog() {
  return (
    <ScrollTimeline className="mx-auto max-w-xl">
      {releases.map((r) => (
        <ScrollTimelineItem key={r.version} date={r.date} title={r.version}>
          {r.note}
        </ScrollTimelineItem>
      ))}
    </ScrollTimeline>
  );
}

Light nodes up earlier

activationLine is where the trigger line sits in the viewport, defaulting to 0.5 (the middle). Moving it down to 0.75 lights a node once it reaches the lower quarter of the screen, which suits timelines with short entries.

<ScrollTimeline activationLine={0.75}>
  {/* ScrollTimelineItem… */}
</ScrollTimeline>

Inside a fixed-height, keyboard-scrollable area

When the timeline sits in a fixed-height area such as a sidebar or dialog, pass the scroll container's ref to container, and add tabIndex and aria-label so keyboard users can focus it and scroll with the arrow keys.

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div
  ref={scrollerRef}
  tabIndex={0}
  aria-label="Project history"
  className="h-[360px] overflow-y-auto rounded-xl border p-6"
>
  <ScrollTimeline container={scrollerRef}>{/* ScrollTimelineItem… */}</ScrollTimeline>
</div>

Props

ScrollTimeline

PropTypeDefaultDescription
childrenReact.ReactNode—A set of ScrollTimelineItem elements
containerReact.RefObject<HTMLElement | null>—Ref of the nested overflow scroll container; when omitted, the window is the scroll container
activationLinenumber0.5Position of the trigger line within the scroll viewport (0 = top, 1 = bottom); both the tip of the growing line and node activation use it as their reference
classNamestring—Appended to the class of the outermost container

ScrollTimelineItem

PropTypeDefaultDescription
titleReact.ReactNode—Node title
dateReact.ReactNode—Date or stage label, shown above the title
iconReact.ReactNode—Node icon (a lucide-react icon is recommended), shown inside the node once it lights up
childrenReact.ReactNode—Body text describing the node
classNamestring—Appended to the class of the node's li

How it works

  • The tip of the growing line and the moment each node lights up both use the same "trigger line" (activationLine) as their reference, so no matter how much the nodes differ in height, a node always lights up the instant the growing line reaches it — the two stay precisely in sync.
  • The end of the main axis carries a "comet tail" glow that follows the leading edge of the growing line, reinforcing the sense of scroll direction.
  • Nodes and their content brighten smoothly from faded as the scroll progresses; nodes not yet reached stay as hollow shapes with a pale outline.

Accessibility

  • When the user has "reduce motion" enabled at the system level, the timeline is presented statically with the whole line lit and all content visible, and it does not change with the scroll.
  • The axis, growing line, nodes, and glow are decorative only, so they are marked aria-hidden; the timeline content is carried by a semantic ol / li structure, which assistive technology reads out as a complete list.
  • The nested scroll container example adds tabIndex and an aria-label so keyboard users can focus it and scroll with the arrow keys.

FAQ

Does it work with the Next.js App Router?

Yes. The file starts with "use client", so import it from a Server Component page as usual and pass your data in as props and children.

The line does not move inside my scrollable container. Why?

Without container, the component tracks the window's scroll. If the element that actually scrolls is an overflow-y-auto box, pass that element's ref to container.

Will nodes of different heights fall out of sync?

No. The end of the growing line and each node's activation both use the same activationLine, independent of node height, so a node lights up exactly when the line reaches it.

How do I change the colors?

The rail, nodes and moving dot use fixed neutral Tailwind classes with dark: variants. To use brand colors, edit those classes in the source; use className for outer spacing and width.

What happens with reduced motion?

The timeline renders as a static, fully lit list with all content visible. It does not react to scrolling, and the dot that follows the line is not shown.