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>
  );
}