byDefaultHuman
Components

Timeline

A vertical event list with rough circle nodes and connecting lines.

Animated Demo

  1. Order placed9:00 AM

    Payment confirmed.

  2. Processing9:15 AM

    Items picked and packed.

  3. Out for delivery11:30 AM

    Your driver is on the way.

  4. Delivered2:04 PM

    Left at front door.

Installation

package manager
npx shadcn add https://bydefaulthuman.fun/r/timeline.json

Props

Timeline

PropTypeDefaultDescription
theme
"pencil" | "ink" | "crayon"
—
Overrides the global Crumble theme for all items.
stroke
string
—
CSS color for rough strokes.
strokeMuted
string
—
CSS color for muted strokes (connector lines).
fill
string
—
CSS color for rough fills.

TimelineItem

PropTypeDefaultDescription
title*
ReactNode
—
Primary label for this timeline entry.
status
"complete" | "active" | "pending"
"pending"
Controls the node appearance — filled circle for complete, outlined with dot for active, empty for pending.
description
ReactNode
—
Secondary text rendered below the title.
time
ReactNode
—
Timestamp or label rendered to the right of the title.
isLast
boolean
false
Suppresses the connector line below this item. Set on the final entry.
id
string
—
Stable ID for seeding the rough node and line drawings.

Static Example

  1. Order placed9:00 AM

    Payment confirmed.

  2. Processing9:15 AM

    Items picked and packed.

  3. Out for delivery11:30 AM

    Your driver is on the way.

  4. Delivered

Statuses

  • complete — filled hachure circle with a tick
  • active — outlined circle with filled inner dot
  • pending — empty outlined circle

Accessibility

Timeline is primarily visual, so the text inside each item should still make sense when read linearly. Include timestamps or status text directly in the item copy when chronology matters.

Examples

The demo above auto-plays on mount and exposes a Replay button. It drives status changes through a plain setTimeout stagger — no extra dependencies required.

// components/demo/TimelineDemo.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import {
  Timeline,
  TimelineItem,
  type TimelineStatus,
} from "@/registry/new-york/ui/timeline";

interface Step {
  title: string;
  description?: string;
  time?: string;
  status: TimelineStatus;
}

const STEPS: Omit<Step, "status">[] = [
  { title: "Order placed", description: "Payment confirmed.", time: "9:00 AM" },
  {
    title: "Processing",
    description: "Items picked and packed.",
    time: "9:15 AM",
  },
  {
    title: "Out for delivery",
    description: "Your driver is on the way.",
    time: "11:30 AM",
  },
  { title: "Delivered", description: "Left at front door.", time: "2:04 PM" },
];

function deriveStatuses(activeIdx: number): TimelineStatus[] {
  return STEPS.map((_, i) => {
    if (i < activeIdx) return "complete";
    if (i === activeIdx) return "active";
    return "pending";
  });
}

export function TimelineDemo() {
  const [activeIdx, setActiveIdx] = useState<number>(-1);
  const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);

  const runAnimation = () => {
    timersRef.current.forEach(clearTimeout);
    timersRef.current = [];
    setActiveIdx(-1);

    STEPS.forEach((_, i) => {
      const t = setTimeout(() => setActiveIdx(i), (i + 1) * 900);
      timersRef.current.push(t);
    });
  };

  useEffect(() => {
    runAnimation();
    return () => timersRef.current.forEach(clearTimeout);
  }, []);

  const statuses =
    activeIdx === -1
      ? STEPS.map((): TimelineStatus => "pending")
      : deriveStatuses(activeIdx);

  return (
    <div className="flex flex-col items-start gap-4 w-full max-w-xs">
      <Timeline>
        {STEPS.map((step, i) => (
          <TimelineItem
            key={step.title}
            title={step.title}
            description={step.description}
            time={step.time}
            status={statuses[i]}
            isLast={i === STEPS.length - 1}
          />
        ))}
      </Timeline>

      <button
        onClick={runAnimation}
        className="self-start rounded-full border border-border px-3 py-1 text-xs text-muted-foreground hover:text-foreground hover:border-foreground/40 transition-colors"
      >
        Replay ↺
      </button>
    </div>
  );
}