Timeline Steps

PreviousNext

A component for displaying chronological events, activities, or steps

Docs
abuicomponent

Preview

Loading preview…
registry/abui/ui/timeline-steps.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

/* -----------------------------------------------------------------------------
 * Timeline (root container)
 * -------------------------------------------------------------------------- */

const timelineStepsVariants = cva("flex", {
  variants: {
    orientation: {
      vertical: "flex-col",
      horizontal: "flex-row overflow-x-auto",
    },
    position: {
      left: "",
      right: "",
      alternate: "",
    },
  },
  defaultVariants: {
    orientation: "vertical",
    position: "left",
  },
})

interface TimelineStepsProps extends React.ComponentProps<"div">, VariantProps<typeof timelineStepsVariants> {}

function TimelineSteps({ className, orientation, position, ...props }: TimelineStepsProps) {
  return (
    <div
      data-slot="timeline-steps"
      data-orientation={orientation}
      data-position={position}
      className={cn(timelineStepsVariants({ orientation, position }), className)}
      {...props}
    />
  )
}

/* -----------------------------------------------------------------------------
 * TimelineItem
 * -------------------------------------------------------------------------- */

const timelineStepsItemVariants = cva("relative flex flex-col", {
  variants: {
    orientation: {
      vertical: "pb-8 last:pb-0",
      horizontal: "flex-1 items-center",
    },
    status: {
      default: "",
      completed: "",
      current: "",
      upcoming: "opacity-60",
    },
  },
  defaultVariants: {
    orientation: "vertical",
    status: "default",
  },
})

interface TimelineStepsItemProps extends React.ComponentProps<"div">, VariantProps<typeof timelineStepsItemVariants> {}

function TimelineStepsItem({ className, orientation, status, ...props }: TimelineStepsItemProps) {
  return (
    <div
      data-slot="timeline-steps-item"
      data-status={status}
      className={cn(timelineStepsItemVariants({ orientation, status }), className)}
      {...props}
    />
  )
}

/* -----------------------------------------------------------------------------
 * TimelineConnector (the line connecting items)
 * -------------------------------------------------------------------------- */

const timelineStepsConnectorVariants = cva("", {
  variants: {
    orientation: {
      vertical:
        "absolute left-[calc(var(--timeline-steps-icon-size,2.5rem)/2)] top-[var(--timeline-steps-icon-size,2.5rem)] h-[calc(100%-var(--timeline-steps-icon-size,2.5rem))] w-px -translate-x-1/2",
      horizontal: "absolute top-3 left-[calc(50%+0.75rem)] h-px w-[calc(100%-1.5rem)]",
    },
    variant: {
      default: "bg-border",
      dashed: "border-l border-dashed border-border bg-transparent",
      dotted: "border-l border-dotted border-border bg-transparent",
    },
    status: {
      default: "",
      completed: "bg-primary",
      current: "bg-gradient-to-b from-primary to-border",
      upcoming: "bg-muted",
    },
  },
  defaultVariants: {
    orientation: "vertical",
    variant: "default",
    status: "default",
  },
})

interface TimelineStepsConnectorProps
  extends React.ComponentProps<"div">,
    VariantProps<typeof timelineStepsConnectorVariants> {}

function TimelineStepsConnector({ className, orientation, variant, status, ...props }: TimelineStepsConnectorProps) {
  return (
    <div
      data-slot="timeline-steps-connector"
      aria-hidden="true"
      className={cn(timelineStepsConnectorVariants({ orientation, variant, status }), className)}
      {...props}
    />
  )
}

/* -----------------------------------------------------------------------------
 * TimelineHeader (contains icon and title row)
 * -------------------------------------------------------------------------- */

function TimelineStepsHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="timeline-steps-header" className={cn("flex items-center gap-3", className)} {...props} />
}

/* -----------------------------------------------------------------------------
 * TimelineStepsIcon (the dot/icon indicator)
 * -------------------------------------------------------------------------- */

const timelineStepsIconVariants = cva(
  "relative z-10 flex shrink-0 items-center justify-center rounded-full border bg-background [--timeline-steps-icon-size:2.5rem]",
  {
    variants: {
      size: {
        sm: "size-6 [--timeline-steps-icon-size:1.5rem] [&>svg]:size-3",
        default: "size-10 [--timeline-steps-icon-size:2.5rem] [&>svg]:size-4",
        lg: "size-12 [--timeline-steps-icon-size:3rem] [&>svg]:size-5",
      },
      variant: {
        default: "border-border text-muted-foreground",
        primary: "border-primary bg-primary text-primary-foreground",
        secondary: "border-secondary bg-secondary text-secondary-foreground",
        destructive: "border-destructive bg-destructive text-destructive-foreground",
        outline: "border-border bg-background text-foreground",
      },
    },
    defaultVariants: {
      size: "default",
      variant: "default",
    },
  },
)

interface TimelineStepsIconProps extends React.ComponentProps<"div">, VariantProps<typeof timelineStepsIconVariants> {}

function TimelineStepsIcon({ className, size, variant, ...props }: TimelineStepsIconProps) {
  return (
    <div
      data-slot="timeline-steps-icon"
      className={cn(timelineStepsIconVariants({ size, variant }), className)}
      {...props}
    />
  )
}

/* -----------------------------------------------------------------------------
 * TimelineStepsContent (container for description, time, etc.)
 * -------------------------------------------------------------------------- */

function TimelineStepsContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="timeline-steps-content"
      className={cn("ms-[3.25rem] flex flex-col gap-1 pt-0.5 pb-2", className)}
      {...props}
    />
  )
}

/* -----------------------------------------------------------------------------
 * TimelineStepsTitle
 * -------------------------------------------------------------------------- */

function TimelineStepsTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="timeline-steps-title"
      className={cn("text-foreground leading-none font-medium tracking-tight", className)}
      {...props}
    />
  )
}

/* -----------------------------------------------------------------------------
 * TimelineDescription
 * -------------------------------------------------------------------------- */

function TimelineStepsDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div data-slot="timeline-steps-description" className={cn("text-muted-foreground text-sm", className)} {...props} />
  )
}

/* -----------------------------------------------------------------------------
 * TimelineStepsTime (timestamp display)
 * -------------------------------------------------------------------------- */

function TimelineStepsTime({ className, ...props }: React.ComponentProps<"time">) {
  return <time data-slot="timeline-steps-time" className={cn("text-muted-foreground text-xs", className)} {...props} />
}

/* -----------------------------------------------------------------------------
 * Exports
 * -------------------------------------------------------------------------- */

export {
  TimelineSteps,
  TimelineStepsItem,
  TimelineStepsConnector,
  TimelineStepsHeader,
  TimelineStepsIcon,
  TimelineStepsContent,
  TimelineStepsTitle,
  TimelineStepsDescription,
  TimelineStepsTime,
  timelineStepsVariants,
  timelineStepsItemVariants,
  timelineStepsConnectorVariants,
  timelineStepsIconVariants,
}

Installation

npx shadcn@latest add @abui/timeline-steps

Usage

import { TimelineSteps } from "@/components/timeline-steps"
<TimelineSteps />