Timeline
A visual representation of events in chronological order.
import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, date: "March 2024", title: "Project Initialized", description: "Set up the repository and the initial architecture.", }, { id: 2, date: "April 2024", title: "Design System", description: "Built the component library and design tokens.", }, { id: 3, date: "May 2024", title: "Beta Release", description: "Shipped the first beta to a group of early adopters.", }, { id: 4, date: "June 2024", title: "Public Launch", description: "Opened the product up to everyone.", },];export default function TimelineDemo() { return ( <Timeline defaultValue={3} className="max-w-lg"> {items.map((item) => ( <TimelineItem key={item.id} step={item.id}> <TimelineHeader> <TimelineDate>{item.date}</TimelineDate> <TimelineTitle>{item.title}</TimelineTitle> </TimelineHeader> <TimelineIndicator /> <TimelineSeparator /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ))} </Timeline> );}Installation
npx shadcn@latest add @cortexcn/timelinenpm install @base-ui/react"use client";import { mergeProps } from "@base-ui/react/merge-props";import { useRender } from "@base-ui/react/use-render";import { createContext, useCallback, useContext, useState } from "react";import { cn } from "@/lib/utils";// Typestype TimelineContextValue = { activeStep: number; setActiveStep: (step: number) => void;};// Contextconst TimelineContext = createContext<TimelineContextValue | undefined>( undefined,);const useTimeline = () => { const context = useContext(TimelineContext); if (!context) { throw new Error("useTimeline must be used within a Timeline"); } return context;};// Componentsinterface TimelineProps extends useRender.ComponentProps<"div"> { defaultValue?: number; value?: number; onValueChange?: (value: number) => void; orientation?: "horizontal" | "vertical";}function Timeline({ defaultValue = 1, value, onValueChange, orientation = "vertical", className, render, children, ...props}: TimelineProps) { const [activeStep, setInternalStep] = useState(defaultValue); const setActiveStep = useCallback( (step: number) => { if (value === undefined) { setInternalStep(step); } onValueChange?.(step); }, [value, onValueChange], ); const currentStep = value ?? activeStep; const defaultProps = { className: cn( "group/timeline flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col", className, ), "data-orientation": orientation, "data-slot": "timeline", children, }; return ( <TimelineContext.Provider value={{ activeStep: currentStep, setActiveStep }} > {useRender({ defaultTagName: "div", render, props: mergeProps<"div">(defaultProps, props), })} </TimelineContext.Provider> );}// TimelineContentfunction TimelineContent({ className, render, children, ...props}: useRender.ComponentProps<"div">) { const defaultProps = { className: cn("text-muted-foreground text-sm", className), "data-slot": "timeline-content", children, }; return useRender({ defaultTagName: "div", render, props: mergeProps<"div">(defaultProps, props), });}// TimelineDatetype TimelineDateProps = useRender.ComponentProps<"time">;function TimelineDate({ className, render, children, ...props}: TimelineDateProps) { const defaultProps = { className: cn( "mb-1 block font-medium text-muted-foreground text-xs group-data-[orientation=vertical]/timeline:max-sm:h-4", className, ), "data-slot": "timeline-date", children, }; return useRender({ defaultTagName: "time", render, props: mergeProps<"time">(defaultProps, props), });}// TimelineHeaderfunction TimelineHeader({ className, render, children, ...props}: useRender.ComponentProps<"div">) { const defaultProps = { className: cn(className), "data-slot": "timeline-header", children, }; return useRender({ defaultTagName: "div", render, props: mergeProps<"div">(defaultProps, props), });}// TimelineIndicatortype TimelineIndicatorProps = useRender.ComponentProps<"div">;function TimelineIndicator({ className, children, render, ...props}: TimelineIndicatorProps) { const defaultProps = { "aria-hidden": true, className: cn( "group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:-translate-x-1/2 absolute size-4 rounded-full border-2 border-primary/20 group-data-[orientation=vertical]/timeline:top-0 group-data-[orientation=horizontal]/timeline:left-0 group-data-completed/timeline-item:border-primary", className, ), "data-slot": "timeline-indicator", children, }; return useRender({ defaultTagName: "div", render, props: mergeProps<"div">(defaultProps, props), });}// TimelineIteminterface TimelineItemProps extends useRender.ComponentProps<"div"> { step: number;}function TimelineItem({ step, className, render, children, ...props}: TimelineItemProps) { const { activeStep } = useTimeline(); const defaultProps = { className: cn( "group/timeline-item relative flex flex-1 flex-col gap-0.5 group-data-[orientation=vertical]/timeline:ms-8 group-data-[orientation=horizontal]/timeline:mt-8 group-data-[orientation=horizontal]/timeline:not-last:pe-8 group-data-[orientation=vertical]/timeline:not-last:pb-6 has-[+[data-completed]]:**:data-[slot=timeline-separator]:bg-primary", className, ), "data-completed": step <= activeStep || undefined, "data-slot": "timeline-item", children, }; return useRender({ defaultTagName: "div", render, props: mergeProps<"div">(defaultProps, props), });}// TimelineSeparatorfunction TimelineSeparator({ className, render, children, ...props}: useRender.ComponentProps<"div">) { const defaultProps = { "aria-hidden": true, className: cn( "group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:-translate-x-1/2 absolute self-start bg-primary/10 group-last/timeline-item:hidden group-data-[orientation=horizontal]/timeline:h-0.5 group-data-[orientation=vertical]/timeline:h-[calc(100%-1rem-0.25rem)] group-data-[orientation=horizontal]/timeline:w-[calc(100%-1rem-0.25rem)] group-data-[orientation=vertical]/timeline:w-0.5 group-data-[orientation=horizontal]/timeline:translate-x-4.5 group-data-[orientation=vertical]/timeline:translate-y-4.5", className, ), "data-slot": "timeline-separator", children, }; return useRender({ defaultTagName: "div", render, props: mergeProps<"div">(defaultProps, props), });}// TimelineTitlefunction TimelineTitle({ className, render, children, ...props}: useRender.ComponentProps<"h3">) { const defaultProps = { className: cn("font-medium text-sm", className), "data-slot": "timeline-title", children, }; return useRender({ defaultTagName: "h3", render, props: mergeProps<"h3">(defaultProps, props), });}export { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,};Usage
import {
Timeline,
TimelineContent,
TimelineDate,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
TimelineTitle,
} from "@/components/ui/timeline";<Timeline>
<TimelineItem step={1}>
<TimelineHeader>
<TimelineDate>March 2024</TimelineDate>
<TimelineTitle>Project Initialized</TimelineTitle>
</TimelineHeader>
<TimelineIndicator />
<TimelineSeparator />
<TimelineContent>
Successfully set up the project repository and initial architecture.
</TimelineContent>
</TimelineItem>
</Timeline>Examples
With Left-Aligned Dates
Move the date into a gutter on the left by widening the item and positioning the
TimelineDate absolutely.
import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, date: "09:00", title: "Doors open", description: "Check in at the front desk and grab a coffee.", }, { id: 2, date: "10:30", title: "Keynote", description: "Opening talk on where the platform is heading.", }, { id: 3, date: "13:00", title: "Workshops", description: "Hands-on sessions across three tracks.", }, { id: 4, date: "17:00", title: "Closing", description: "Wrap up, prizes, and networking.", },];export default function TimelineLeftAlignedDates() { return ( <Timeline defaultValue={2} className="max-w-xl"> {items.map((item) => ( <TimelineItem key={item.id} step={item.id} className="group-data-[orientation=vertical]/timeline:ms-24" > <TimelineHeader> <TimelineSeparator /> <TimelineDate className="-left-24 absolute top-0 w-16 text-right"> {item.date} </TimelineDate> <TimelineTitle>{item.title}</TimelineTitle> <TimelineIndicator /> </TimelineHeader> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ))} </Timeline> );}With Custom Indicators
Pass children to TimelineIndicator to render numbers, and use the
group-data-completed/timeline-item variant to style completed steps.
import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, date: "Step 1", title: "Create your account", description: "Sign up with your email and verify your address.", }, { id: 2, date: "Step 2", title: "Set up your workspace", description: "Invite teammates and pick a plan.", }, { id: 3, date: "Step 3", title: "Connect your data", description: "Import sources or start from a template.", }, { id: 4, date: "Step 4", title: "Ship your first project", description: "Publish and share it with your team.", },];export default function TimelineCustomIndicators() { return ( <Timeline defaultValue={2} className="max-w-lg"> {items.map((item) => ( <TimelineItem key={item.id} step={item.id}> <TimelineHeader> <TimelineDate>{item.date}</TimelineDate> <TimelineTitle>{item.title}</TimelineTitle> </TimelineHeader> <TimelineIndicator className="flex size-7 items-center justify-center bg-background text-xs font-medium group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground"> {item.id} </TimelineIndicator> <TimelineSeparator /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ))} </Timeline> );}With Icons
Render an icon inside each TimelineIndicator.
import { Flag, Package, Rocket, Wrench } from "lucide-react";import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, icon: Flag, date: "Q1", title: "Kickoff", description: "Aligned on goals and scoped the roadmap.", }, { id: 2, icon: Wrench, date: "Q2", title: "Build", description: "Implemented the core features and internal tooling.", }, { id: 3, icon: Package, date: "Q3", title: "Package", description: "Hardened the API and prepared the release.", }, { id: 4, icon: Rocket, date: "Q4", title: "Launch", description: "Rolled out to production and announced it.", },];export default function TimelineIcons() { return ( <Timeline defaultValue={2} className="max-w-lg"> {items.map((item) => { const Icon = item.icon; return ( <TimelineItem key={item.id} step={item.id}> <TimelineHeader> <TimelineDate>{item.date}</TimelineDate> <TimelineTitle>{item.title}</TimelineTitle> </TimelineHeader> <TimelineIndicator className="flex size-7 items-center justify-center bg-background group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground"> <Icon className="size-3.5" /> </TimelineIndicator> <TimelineSeparator /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ); })} </Timeline> );}Alternating Layout
Center the rail and alternate items left and right based on their index.
import { cn } from "@/lib/utils";import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, date: "2019", title: "Founded", description: "Started in a small garage with two laptops.", }, { id: 2, date: "2021", title: "Seed Round", description: "Raised our first round and grew the team to ten.", }, { id: 3, date: "2023", title: "1M Users", description: "Crossed a million active users worldwide.", }, { id: 4, date: "2024", title: "Global", description: "Opened offices on three continents.", },];export default function TimelineAlternating() { return ( <Timeline defaultValue={3} className="mx-auto max-w-2xl"> {items.map((item, index) => { const isEven = index % 2 === 0; return ( <TimelineItem key={item.id} step={item.id} className={cn( "group-data-[orientation=vertical]/timeline:ms-0", isEven ? "items-end pe-[calc(50%+1.5rem)] text-right" : "items-start ps-[calc(50%+1.5rem)]", )} > <TimelineHeader> <TimelineDate>{item.date}</TimelineDate> <TimelineTitle>{item.title}</TimelineTitle> </TimelineHeader> <TimelineIndicator className="group-data-[orientation=vertical]/timeline:left-1/2" /> <TimelineSeparator className="group-data-[orientation=vertical]/timeline:left-1/2" /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ); })} </Timeline> );}Pipeline Steps
Drive each step's icon from a status (done, running, queued) to model a build pipeline.
import { Check, Clock, Loader } from "lucide-react";import { cn } from "@/lib/utils";import { Timeline, TimelineContent, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const ACTIVE_STEP = 3;const items = [ { id: 1, title: "Checkout", description: "Cloned the repository at main@a1b2c3d.", }, { id: 2, title: "Install", description: "Installed 482 packages in 12s.", }, { id: 3, title: "Build", description: "Compiling the production bundle...", }, { id: 4, title: "Test", description: "Waiting for the build to finish.", }, { id: 5, title: "Deploy", description: "Queued.", },];function StepIcon({ id }: { id: number }) { if (id < ACTIVE_STEP) { return <Check className="size-3.5" />; } if (id === ACTIVE_STEP) { return <Loader className="size-3.5 animate-spin" />; } return <Clock className="size-3.5 text-muted-foreground" />;}export default function TimelinePipeline() { return ( <Timeline defaultValue={ACTIVE_STEP - 1} className="max-w-lg"> {items.map((item) => ( <TimelineItem key={item.id} step={item.id}> <TimelineHeader> <TimelineTitle className={cn(item.id === ACTIVE_STEP && "text-primary")} > {item.title} </TimelineTitle> </TimelineHeader> <TimelineIndicator className="flex size-7 items-center justify-center bg-background group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground"> <StepIcon id={item.id} /> </TimelineIndicator> <TimelineSeparator /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ))} </Timeline> );}Dot Indicators
Shrink the indicator into a small filled dot.
import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, date: "08:32", title: "Order placed", description: "We received your order and sent a confirmation.", }, { id: 2, date: "11:15", title: "Packed", description: "Your items were picked and packed at the warehouse.", }, { id: 3, date: "14:48", title: "Shipped", description: "The carrier picked up your parcel.", }, { id: 4, date: "Pending", title: "Out for delivery", description: "Arriving soon to your address.", },];export default function TimelineDotIndicators() { return ( <Timeline defaultValue={3} className="max-w-lg"> {items.map((item) => ( <TimelineItem key={item.id} step={item.id}> <TimelineHeader> <TimelineDate>{item.date}</TimelineDate> <TimelineTitle>{item.title}</TimelineTitle> </TimelineHeader> <TimelineIndicator className="size-2.5 border-0 bg-primary/20 group-data-completed/timeline-item:bg-primary" /> <TimelineSeparator /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ))} </Timeline> );}Horizontal Orientation
Set orientation="horizontal" to lay the timeline out left to right.
import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, date: "Mon", title: "Research", description: "Interviewed users and mapped the problem.", }, { id: 2, date: "Tue", title: "Design", description: "Sketched flows and built a prototype.", }, { id: 3, date: "Wed", title: "Develop", description: "Implemented the screens and wired up data.", }, { id: 4, date: "Thu", title: "Review", description: "Tested, fixed bugs, and prepared to ship.", },];export default function TimelineHorizontal() { return ( <Timeline defaultValue={2} orientation="horizontal" className="w-full"> {items.map((item) => ( <TimelineItem key={item.id} step={item.id}> <TimelineHeader> <TimelineDate>{item.date}</TimelineDate> <TimelineTitle>{item.title}</TimelineTitle> </TimelineHeader> <TimelineIndicator /> <TimelineSeparator /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ))} </Timeline> );}Horizontal with Top Indicators
Combine the horizontal orientation with numbered indicators and centered text.
import { Timeline, TimelineContent, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle,} from "@/components/ui/timeline";const items = [ { id: 1, title: "Cart", description: "Review the items in your cart.", }, { id: 2, title: "Shipping", description: "Add your delivery address.", }, { id: 3, title: "Payment", description: "Choose how you want to pay.", }, { id: 4, title: "Done", description: "Confirm and place your order.", },];export default function TimelineHorizontalTopIndicators() { return ( <Timeline defaultValue={2} orientation="horizontal" className="w-full"> {items.map((item) => ( <TimelineItem key={item.id} step={item.id} className="text-center"> <TimelineHeader> <TimelineTitle>{item.title}</TimelineTitle> </TimelineHeader> <TimelineIndicator className="flex size-7 items-center justify-center bg-background text-xs font-medium group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground"> {item.id} </TimelineIndicator> <TimelineSeparator className="group-data-[orientation=horizontal]/timeline:translate-x-7" /> <TimelineContent>{item.description}</TimelineContent> </TimelineItem> ))} </Timeline> );}API Reference
Timeline
The root component for the timeline.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number | 1 | The initial active step. |
value | number | - | The current active step (controlled). |
onValueChange | (value: number) => void | - | Callback fired when the active step changes. |
orientation | "horizontal" | "vertical" | "vertical" | The layout orientation of the timeline. |
TimelineItem
A single item in the timeline.
| Prop | Type | Default | Description |
|---|---|---|---|
step | number | - | Required. The step number for this item. |
TimelineDate
The date or time label for a timeline item.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the date label. |
TimelineTitle
The title for a timeline item.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the title. |
TimelineIndicator
The visual indicator (usually a dot) for a timeline item.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the indicator. |
TimelineSeparator
The line connecting timeline indicators.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the separator line. |
TimelineHeader
A container for the date and title.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the header container. |
TimelineContent
The main descriptive content for a timeline item.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the content container. |