Timeline
Vertical event timeline with default or icon dots, tone variants and labelled time / title / description parts.
Example
Installation
npx shadcn@latest add https://hirael.com/r/timeline.jsonAPI
<Timeline />
+ native element propsNo props of its own — forwards everything to the underlying element.
<TimelineItem />
+ native element propsNo props of its own — forwards everything to the underlying element.
<TimelineDot />
+ native element props| Prop | Type | Default |
|---|---|---|
tone | "default" | "muted" | "success" | "warning" | "danger" | null | "default" |
children | React.ReactNode | — |
<TimelineContent />
+ native element propsNo props of its own — forwards everything to the underlying element.
<TimelineTitle />
+ native element propsNo props of its own — forwards everything to the underlying element.
<TimelineTime />
+ native element propsNo props of its own — forwards everything to the underlying element.
<TimelineDescription />
+ native element propsNo props of its own — forwards everything to the underlying element.
Component source
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
type TimelineProps = React.ComponentProps<"ol">;
function Timeline({ className, ...props }: TimelineProps) {
return (
<ol
data-slot="timeline"
className={cn("relative flex flex-col", className)}
{...props}
/>
);
}
type TimelineItemProps = React.ComponentProps<"li">;
function TimelineItem({ className, ...props }: TimelineItemProps) {
return (
<li
data-slot="timeline-item"
className={cn(
"group relative flex gap-4 pb-6 last:pb-0",
"before:absolute before:start-[7px] before:top-4 before:bottom-0 before:w-px before:bg-border",
"last:before:hidden",
className,
)}
{...props}
/>
);
}
const timelineDotVariants = cva(
"relative z-10 mt-1.5 inline-flex size-[15px] shrink-0 items-center justify-center rounded-full ring-2 ring-background",
{
variants: {
tone: {
default: "bg-foreground",
muted: "bg-muted-foreground",
success: "bg-success",
warning: "bg-warning",
danger: "bg-destructive",
},
},
defaultVariants: {
tone: "default",
},
},
);
type TimelineDotProps = Omit<React.ComponentProps<"span">, "children"> &
VariantProps<typeof timelineDotVariants> & {
children?: React.ReactNode;
};
function TimelineDot({
className,
tone = "default",
children,
...props
}: TimelineDotProps) {
if (children) {
return (
<span
data-slot="timeline-dot"
data-tone={tone}
className={cn(
"relative z-10 mt-0.5 inline-flex size-6 -translate-x-[5px] rtl:translate-x-[5px] items-center justify-center rounded-full bg-background ring-1 ring-border [&_svg]:size-3",
className,
)}
{...props}
>
{children}
</span>
);
}
return (
<span
data-slot="timeline-dot"
data-tone={tone}
className={cn(timelineDotVariants({ tone }), className)}
{...props}
/>
);
}
type TimelineContentProps = React.ComponentProps<"div">;
function TimelineContent({ className, ...props }: TimelineContentProps) {
return (
<div
data-slot="timeline-content"
className={cn("flex min-w-0 flex-1 flex-col gap-1 pt-0.5", className)}
{...props}
/>
);
}
type TimelineTitleProps = React.ComponentProps<"p">;
function TimelineTitle({ className, ...props }: TimelineTitleProps) {
return (
<p
data-slot="timeline-title"
className={cn("text-sm font-medium text-foreground", className)}
{...props}
/>
);
}
type TimelineTimeProps = React.ComponentProps<"time">;
function TimelineTime({ className, ...props }: TimelineTimeProps) {
return (
<time
data-slot="timeline-time"
className={cn(
"font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground",
className,
)}
{...props}
/>
);
}
type TimelineDescriptionProps = React.ComponentProps<"p">;
function TimelineDescription({
className,
...props
}: TimelineDescriptionProps) {
return (
<p
data-slot="timeline-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
export {
Timeline,
TimelineItem,
TimelineDot,
TimelineContent,
TimelineTitle,
TimelineTime,
TimelineDescription,
};