Skip to content
6

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.json

Usage

import {
  Timeline,
  TimelineItem,
  TimelineDot,
  TimelineContent,
  TimelineTitle,
  TimelineTime,
  TimelineDescription,
} from "@/components/timeline"

Paths assume the default @/ alias from your components.json. The example above shows the parts composed.

API

<Timeline />

+ native element props

No props of its own; forwards everything to the underlying element.

<TimelineItem />

+ native element props

No props of its own; forwards everything to the underlying element.

<TimelineDot />

+ native element props
PropTypeDefault
childrenReact.ReactNodenone
tone"default" | "muted" | "success" | "warning" | "danger" | null'default'

<TimelineContent />

+ native element props

No props of its own; forwards everything to the underlying element.

<TimelineTitle />

+ native element props

No props of its own; forwards everything to the underlying element.

<TimelineTime />

+ native element props

No props of its own; forwards everything to the underlying element.

<TimelineDescription />

+ native element props

No 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'>;

const Timeline = ({ className, ...props }: TimelineProps) => {
  return <ol data-slot="timeline" className={cn('relative flex flex-col', className)} {...props} />;
};

type TimelineItemProps = React.ComponentProps<'li'>;

const 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',
    },
  },
);

interface TimelineDotProps
  extends Omit<React.ComponentProps<'span'>, 'children'>, VariantProps<typeof timelineDotVariants> {
  children?: React.ReactNode;
}

const 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'>;

const 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'>;

const 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'>;

const 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'>;

const 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 };

Dependencies

npm

class-variance-authority