# Timeline Vertical Activity — React Stepper Component

> An activity feed on a vertical rail: icon nodes pop in one after another while the connecting line draws downward between them.

- Category: [Steppers & Timelines](https://pixelpapa.com/components/steppers.md)
- Tags: timeline, activity, feed, stagger, history
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/steppers/timeline-vertical-activity

## Install

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

The file is written to `components/pixelpapa/timeline-vertical-activity.tsx`. To install manually, run `npm install lucide-react 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 { GitMerge, MessageSquare, Rocket, TriangleAlert, UserPlus, type LucideIcon } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";

type Tone = "primary" | "success" | "warning" | "danger" | "neutral";

export type ActivityItem = {
  id: string;
  icon: LucideIcon;
  tone?: Tone;
  actor: string;
  action: string;
  target?: string;
  time: string;
  /** Optional extra content under the headline: a quote, chips, a diff summary. */
  body?: ReactNode;
};

const tones: Record<Tone, string> = {
  primary: "bg-primary/10 text-primary ring-primary/20",
  success: "bg-emerald-500/10 text-emerald-600 ring-emerald-500/20 dark:text-emerald-400",
  warning: "bg-amber-500/10 text-amber-600 ring-amber-500/25 dark:text-amber-400",
  danger: "bg-destructive/10 text-destructive ring-destructive/20",
  neutral: "bg-muted text-muted-foreground ring-border",
};

type ActivityTimelineProps = {
  items: ActivityItem[];
  /** Seconds between each entry. */
  stagger?: number;
  className?: string;
};

export function ActivityTimeline({ items, stagger = 0.14, className }: ActivityTimelineProps) {
  const reduce = useReducedMotion();
  const ease = [0.22, 1, 0.36, 1] as const;

  return (
    <motion.ol
      initial={reduce ? false : "hidden"}
      whileInView="shown"
      viewport={{ once: true, amount: 0.2 }}
      transition={{ staggerChildren: stagger }}
      className={cn("w-full", className)}
    >
      {items.map(({ id, icon: Icon, tone = "neutral", actor, action, target, time, body }, i) => (
        <motion.li key={id} variants={{ hidden: {}, shown: {} }} className="relative flex gap-3.5 pb-5 last:pb-0">
          {i < items.length - 1 && (
            <motion.span
              aria-hidden
              variants={{ hidden: { scaleY: 0 }, shown: { scaleY: 1 } }}
              transition={{ duration: 0.5, delay: 0.12, ease }}
              className="absolute top-9 bottom-1 left-4 w-px origin-top -translate-x-1/2 bg-border"
            />
          )}
          <motion.span
            variants={{ hidden: { scale: 0, opacity: 0 }, shown: { scale: 1, opacity: 1 } }}
            transition={{ type: "spring", stiffness: 420, damping: 20 }}
            className={cn("flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset", tones[tone])}
          >
            <Icon className="size-4" />
          </motion.span>
          <motion.div
            variants={{ hidden: { opacity: 0, x: -10 }, shown: { opacity: 1, x: 0 } }}
            transition={{ duration: 0.45, ease }}
            className="min-w-0 flex-1 pt-1"
          >
            <div className="flex items-baseline justify-between gap-4">
              <p className="text-sm leading-snug text-muted-foreground">
                <span className="font-medium text-foreground">{actor}</span> {action}{" "}
                {target && <span className="font-medium text-foreground">{target}</span>}
              </p>
              <time className="shrink-0 text-xs whitespace-nowrap text-muted-foreground/80">{time}</time>
            </div>
            {body && <div className="mt-2">{body}</div>}
          </motion.div>
        </motion.li>
      ))}
    </motion.ol>
  );
}

function Chip({ children, className }: { children: ReactNode; className?: string }) {
  return (
    <span className={cn("inline-flex items-center rounded-md border border-border bg-background px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground", className)}>
      {children}
    </span>
  );
}

const activity: ActivityItem[] = [
  {
    id: "a1",
    icon: Rocket,
    tone: "success",
    actor: "Ines Varga",
    action: "deployed",
    target: "checkout-api to production",
    time: "2 min ago",
    body: (
      <div className="flex gap-1.5">
        <Chip>v4.18.2</Chip>
        <Chip>a41c9e2</Chip>
      </div>
    ),
  },
  {
    id: "a2",
    icon: MessageSquare,
    tone: "primary",
    actor: "Tomas Reyl",
    action: "commented on",
    target: "Usage rollups",
    time: "26 min ago",
    body: (
      <p className="rounded-xl rounded-tl-sm border border-border bg-card px-3.5 py-2.5 text-sm text-card-foreground shadow-xs">
        Weekly buckets look right. Can we default the chart to UTC so finance sees the same totals?
      </p>
    ),
  },
  {
    id: "a3",
    icon: GitMerge,
    tone: "neutral",
    actor: "Mira Osei",
    action: "merged",
    target: "chore/tokenizer-3.2",
    time: "1 hr ago",
    body: (
      <div className="flex gap-1.5">
        <Chip className="text-emerald-600 dark:text-emerald-400">+248</Chip>
        <Chip className="text-destructive">−61</Chip>
        <Chip>14 files</Chip>
      </div>
    ),
  },
  { id: "a4", icon: TriangleAlert, tone: "warning", actor: "Alert", action: "p95 latency passed 800 ms on", target: "search-indexer", time: "3 hr ago" },
  { id: "a5", icon: UserPlus, tone: "neutral", actor: "Jun Okabe", action: "invited 3 teammates to", target: "Platform", time: "Yesterday" },
];

export default function TimelineVerticalActivityDemo() {
  return (
    <div className="w-full max-w-lg">
      <div className="mb-4 flex items-center gap-3">
        <h3 className="text-sm font-semibold text-foreground">Today</h3>
        <span className="h-px flex-1 bg-border" />
        <span className="text-xs text-muted-foreground">Thursday, Nov 12</span>
      </div>
      <ActivityTimeline items={activity} />
    </div>
  );
}
```

## AI prompt

```text
Add the "Timeline Vertical Activity" component from PixelPapa UI to this project.

What it is: An activity feed on a vertical rail: icon nodes pop in one after another while the connecting line draws downward between them.
Category: Steppers & Timelines. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/timeline-vertical-activity.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/timeline-vertical-activity.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/timeline-vertical-activity" 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 steppers & timelines

- [Stepper Horizontal Progress](https://pixelpapa.com/components/steppers/stepper-horizontal-progress.md): A horizontal stepper where the connector line fills toward the next step and each completed circle draws its checkmark.
- [Stepper Order Tracking](https://pixelpapa.com/components/steppers/stepper-order-tracking.md): A shipment tracker where a truck drives along a curved SVG route to the current stage, leaving a solid line behind it and lighting each stop it passes.
- [Stepper Vertical Details](https://pixelpapa.com/components/steppers/stepper-vertical-details.md) (Pro): A vertical stepper where the active step springs open to show its details while finished steps collapse to a one-line summary and the rail fills.
- [Stepper Wizard Form](https://pixelpapa.com/components/steppers/stepper-wizard-form.md) (Pro): A multi-step form that slides between steps in the direction of travel and shakes, with inline errors, when a step fails validation.
- [Timeline Horizontal Milestones](https://pixelpapa.com/components/steppers/timeline-horizontal-milestones.md) (Pro): A draggable horizontal roadmap: the track glides to centre the chosen milestone, a ring marker slides between dots, and reached milestones fill the line.
