Components
Timeline
A vertical event list with rough circle nodes and connecting lines.
Animated Demo
- Order placed9:00 AM
Payment confirmed.
- Processing9:15 AM
Items picked and packed.
- Out for delivery11:30 AM
Your driver is on the way.
- Delivered2:04 PM
Left at front door.
Installation
package manager
npx shadcn add https://bydefaulthuman.fun/r/timeline.jsonProps
Timeline
PropTypeDefaultDescription
theme"pencil" | "ink" | "crayon"—
Overrides the global Crumble theme for all items.
strokestring—
CSS color for rough strokes.
strokeMutedstring—
CSS color for muted strokes (connector lines).
fillstring—
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.
descriptionReactNode—
Secondary text rendered below the title.
timeReactNode—
Timestamp or label rendered to the right of the title.
isLastbooleanfalseSuppresses the connector line below this item. Set on the final entry.
idstring—
Stable ID for seeding the rough node and line drawings.
Static Example
- Order placed9:00 AM
Payment confirmed.
- Processing9:15 AM
Items picked and packed.
- Out for delivery11:30 AM
Your driver is on the way.
- Delivered
Statuses
complete— filled hachure circle with a tickactive— outlined circle with filled inner dotpending— 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>
);
}