# Activity Timeline

> Activity timeline with icon or dot markers that fade in one by one.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/activity-timeline
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/activity-timeline.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/activity-timeline-demo.tsx"
import { GitMerge, GitPullRequest, MessageSquare, Rocket, Tag } from "lucide-react"

import { ActivityTimeline } from "@/components/activity-timeline"

export default function ActivityTimelineDemo() {
  return (
    <div className="w-full max-w-md rounded-2xl border bg-card p-6 shadow-sm">
      <h3 className="mb-5 text-sm font-semibold">Activity</h3>
      <ActivityTimeline
        items={[
          {
            icon: <Rocket />,
            color: "#22c55e",
            title: (
              <>
                <span className="font-medium">v2.4.0</span> deployed to production
              </>
            ),
            time: "2m ago",
          },
          {
            icon: <GitMerge />,
            color: "#a855f7",
            title: (
              <>
                <span className="font-medium">Ada</span> merged #482
              </>
            ),
            description: "feat: command palette fuzzy ranking",
            time: "1h ago",
          },
          {
            icon: <MessageSquare />,
            color: "#3b82f6",
            title: (
              <>
                <span className="font-medium">Grace</span> commented on #479
              </>
            ),
            description: (
              <p className="mt-1 rounded-lg border bg-muted/40 px-3 py-2 text-foreground">
                Looks great — can we keep the old shortcut as an alias?
              </p>
            ),
            time: "3h ago",
          },
          {
            icon: <GitPullRequest />,
            color: "#f97316",
            title: (
              <>
                <span className="font-medium">Alan</span> opened #479
              </>
            ),
            time: "Yesterday",
          },
          {
            icon: <Tag />,
            title: (
              <>
                Release <span className="font-medium">v2.3.2</span> tagged
              </>
            ),
            time: "Nov 3",
          },
        ]}
      />
    </div>
  )
}
```

## Source

```tsx title="components/activity-timeline.tsx"
import * as React from "react"

import { cn } from "@/lib/utils"

export interface TimelineItem {
  title: React.ReactNode
  description?: React.ReactNode
  time: string
  /** Icon or initials rendered inside the marker */
  icon?: React.ReactNode
  /** Marker color */
  color?: string
}

export interface ActivityTimelineProps extends React.ComponentProps<"ol"> {
  items: TimelineItem[]
  /** "default" uses icon markers, "compact" uses small dots */
  variant?: "default" | "compact"
  /** Fade the items in one after another */
  animate?: boolean
  /** Delay between items in ms */
  stagger?: number
}

export function ActivityTimeline({
  items,
  variant = "default",
  animate = true,
  stagger = 80,
  className,
  ...props
}: ActivityTimelineProps) {
  const compact = variant === "compact"

  return (
    <ol className={cn("flex flex-col", className)} {...props}>
      {items.map((item, index) => (
        <li
          key={index}
          style={animate ? { animationDelay: `${index * stagger}ms` } : undefined}
          className={cn(
            "relative flex gap-4",
            compact ? "pb-5" : "pb-7",
            "last:pb-0",
            animate && "animate-in fade-in slide-in-from-bottom-2 fill-mode-both duration-500"
          )}
        >
          {index < items.length - 1 && (
            <span
              aria-hidden
              className={cn("absolute w-px bg-border", compact ? "top-4 bottom-0 left-[5px]" : "top-9 bottom-1 left-4")}
            />
          )}
          {compact ? (
            <span
              aria-hidden
              className="relative mt-1.5 size-[11px] shrink-0 rounded-full border-2 border-background ring-1 ring-border"
              style={{ background: item.color ?? "var(--muted-foreground)" }}
            />
          ) : (
            <span
              aria-hidden
              className="relative flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold [&_svg]:size-4"
              style={{
                color: item.color ?? "var(--muted-foreground)",
                background: `color-mix(in oklab, ${item.color ?? "var(--muted-foreground)"} 14%, var(--background))`,
              }}
            >
              {item.icon}
            </span>
          )}
          <div className={cn("flex min-w-0 flex-1 flex-col gap-1", !compact && "pt-1.5")}>
            <div className="flex items-baseline justify-between gap-4">
              <p className="text-sm">{item.title}</p>
              <time className="shrink-0 text-xs text-muted-foreground tabular-nums">{item.time}</time>
            </div>
            {item.description && <div className="text-sm text-muted-foreground">{item.description}</div>}
          </div>
        </li>
      ))}
    </ol>
  )
}
```
