# Roadmap Timeline

> A quarterly product roadmap timeline with Shipped, In progress and Planned status pills on a vertical line.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/roadmap-timeline
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/roadmap-timeline.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/roadmap-timeline-demo.tsx"
import { RoadmapTimeline } from "@/components/roadmap-timeline"

export default function RoadmapTimelineDemo() {
  return (
    <div className="w-full max-w-2xl px-4">
      <RoadmapTimeline
        quarters={[
          {
            label: "Q3 2026",
            items: [
              { title: "Team workspaces", description: "Share components and themes across your whole organization.", status: "shipped" },
              { title: "Figma plugin", description: "Import design tokens straight into your project.", status: "shipped" },
            ],
          },
          {
            label: "Q4 2026",
            items: [
              { title: "Visual theme editor", description: "Tune colors, radius and type live and export the CSS.", status: "in-progress" },
              { title: "Component playground", description: "Edit props and see the result instantly.", status: "in-progress" },
            ],
          },
          {
            label: "Q1 2027",
            items: [
              { title: "Public API", description: "Query the registry programmatically.", status: "planned" },
              { title: "Mobile component kit", status: "planned" },
            ],
          },
        ]}
      />
    </div>
  )
}
```

## Source

```tsx title="components/roadmap-timeline.tsx"
import * as React from "react"
import { CircleCheck, CircleDashed, LoaderCircle } from "lucide-react"

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

export type RoadmapStatus = "shipped" | "in-progress" | "planned"

export interface RoadmapItem {
  title: string
  description?: string
  status: RoadmapStatus
}

export interface RoadmapQuarter {
  /** e.g. "Q1 2026" */
  label: string
  items: RoadmapItem[]
}

export interface RoadmapTimelineProps extends React.ComponentProps<"div"> {
  quarters: RoadmapQuarter[]
}

const statusMeta: Record<RoadmapStatus, { label: string; pill: string; icon: React.ReactNode }> = {
  shipped: {
    label: "Shipped",
    pill: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
    icon: <CircleCheck className="size-3.5" aria-hidden />,
  },
  "in-progress": {
    label: "In progress",
    pill: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
    icon: <LoaderCircle className="size-3.5" aria-hidden />,
  },
  planned: {
    label: "Planned",
    pill: "bg-muted text-muted-foreground",
    icon: <CircleDashed className="size-3.5" aria-hidden />,
  },
}

const dotClass: Record<RoadmapStatus, string> = {
  shipped: "bg-emerald-500",
  "in-progress": "bg-amber-500",
  planned: "bg-muted-foreground/40",
}

export function RoadmapTimeline({ quarters, className, ...props }: RoadmapTimelineProps) {
  return (
    <div className={cn("w-full", className)} {...props}>
      <ol className="relative ml-3 border-l border-border">
        {quarters.map((quarter) => (
          <li key={quarter.label} className="relative pb-10 pl-8 last:pb-0">
            <span
              aria-hidden
              className="absolute -left-1.25 top-1.5 size-2.5 rounded-full border-2 border-background bg-foreground ring-1 ring-border"
            />
            <h3 className="text-lg font-semibold tracking-tight">{quarter.label}</h3>
            <ul className="mt-4 grid gap-3">
              {quarter.items.map((item) => {
                const meta = statusMeta[item.status]
                return (
                  <li
                    key={item.title}
                    className="flex items-start gap-3 rounded-xl border bg-card p-4 text-card-foreground shadow-sm"
                  >
                    <span aria-hidden className={cn("mt-1.5 size-2 shrink-0 rounded-full", dotClass[item.status])} />
                    <div className="min-w-0 flex-1">
                      <div className="flex flex-wrap items-center justify-between gap-2">
                        <p className="text-sm font-medium">{item.title}</p>
                        <span
                          className={cn(
                            "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
                            meta.pill
                          )}
                        >
                          {meta.icon}
                          {meta.label}
                        </span>
                      </div>
                      {item.description && (
                        <p className="mt-1 text-sm text-muted-foreground">{item.description}</p>
                      )}
                    </div>
                  </li>
                )
              })}
            </ul>
          </li>
        ))}
      </ol>
    </div>
  )
}
```
