cortexcn

Timeline

A visual representation of events in chronological order.

Loading…
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/timeline
Install the following dependencies:
npm install @base-ui/react
Copy and paste the following code into your project.
components/ui/timeline.tsx
"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,};
Update the import paths to match your project setup.

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.

Loading…
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.

Loading…
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.

Loading…
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.

Loading…
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.

Loading…
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.

Loading…
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.

Loading…
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.

Loading…
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.

PropTypeDefaultDescription
defaultValuenumber1The initial active step.
valuenumber-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.

PropTypeDefaultDescription
stepnumber-Required. The step number for this item.

TimelineDate

The date or time label for a timeline item.

PropTypeDefaultDescription
classNamestring-Additional CSS classes for the date label.

TimelineTitle

The title for a timeline item.

PropTypeDefaultDescription
classNamestring-Additional CSS classes for the title.

TimelineIndicator

The visual indicator (usually a dot) for a timeline item.

PropTypeDefaultDescription
classNamestring-Additional CSS classes for the indicator.

TimelineSeparator

The line connecting timeline indicators.

PropTypeDefaultDescription
classNamestring-Additional CSS classes for the separator line.

TimelineHeader

A container for the date and title.

PropTypeDefaultDescription
classNamestring-Additional CSS classes for the header container.

TimelineContent

The main descriptive content for a timeline item.

PropTypeDefaultDescription
classNamestring-Additional CSS classes for the content container.

On this page