Process Timeline
Numbered steps with a gradient line that draws as you scroll.
How it works
From setup to insight in minutes
- 01
Connect your data
5 minLink your database, warehouse or product events in minutes with guided setup.
- 02
Define what matters
10 minChoose the metrics and goals your team cares about. We build the dashboards.
- 03
Invite your team
2 minShare live views, leave comments and get alerts in the tools you already use.
- 04
Ship with confidence
OngoingMeasure every launch and see exactly what moved the numbers.
components/ui/process-timeline.tsx
"use client";
import { motion } from "motion/react";
type Step = { title: string; text: string; tag?: string };
const steps: Step[] = [
{ title: "Connect your data", text: "Link your database, warehouse or product events in minutes with guided setup.", tag: "5 min" },
{ title: "Define what matters", text: "Choose the metrics and goals your team cares about. We build the dashboards.", tag: "10 min" },
{ title: "Invite your team", text: "Share live views, leave comments and get alerts in the tools you already use.", tag: "2 min" },
{ title: "Ship with confidence", text: "Measure every launch and see exactly what moved the numbers.", tag: "Ongoing" },
];
export function ProcessTimeline({ items = steps }: { items?: Step[] }) {
return (
<ol className="relative">
<motion.span
aria-hidden
className="absolute left-[19px] top-2 bottom-2 w-px origin-top bg-gradient-to-b from-blue-600 via-blue-400 to-zinc-200"
initial={{ scaleY: 0 }}
whileInView={{ scaleY: 1 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 1.4, ease: [0.22, 1, 0.36, 1] }}
/>
{items.map((step, i) => (
<motion.li
key={step.title}
className="relative flex gap-6 pb-10 last:pb-0"
initial={{ opacity: 0, x: -16 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.6, delay: 0.15 + i * 0.15, ease: [0.22, 1, 0.36, 1] }}
>
<span className="relative z-10 grid size-10 shrink-0 place-items-center rounded-full border border-zinc-200 bg-white text-sm font-semibold text-zinc-900 shadow-sm">
{String(i + 1).padStart(2, "0")}
</span>
<div className="pt-1.5">
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-semibold text-zinc-900">{step.title}</h3>
{step.tag && <span className="rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500">{step.tag}</span>}
</div>
<p className="mt-1.5 max-w-md text-sm leading-relaxed text-zinc-500">{step.text}</p>
</div>
</motion.li>
))}
</ol>
);
}
export default function Demo() {
return (
<div className="w-full max-w-xl">
<p className="text-sm font-medium text-blue-600">How it works</p>
<h2 className="mb-10 mt-2 text-3xl font-semibold tracking-tight text-zinc-900">From setup to insight in minutes</h2>
<ProcessTimeline />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/process-timeline.tsxcomponents/ui/process-timeline.tsx"use client"; import { motion } from "motion/react"; type Step = { title: string; text: string; tag?: string }; const steps: Step[] = [ { title: "Connect your data", text: "Link your database, warehouse or product events in minutes with guided setup.", tag: "5 min" }, { title: "Define what matters", text: "Choose the metrics and goals your team cares about. We build the dashboards.", tag: "10 min" }, { title: "Invite your team", text: "Share live views, leave comments and get alerts in the tools you already use.", tag: "2 min" }, { title: "Ship with confidence", text: "Measure every launch and see exactly what moved the numbers.", tag: "Ongoing" }, ]; export function ProcessTimeline({ items = steps }: { items?: Step[] }) { return ( <ol className="relative"> <motion.span aria-hidden className="absolute left-[19px] top-2 bottom-2 w-px origin-top bg-gradient-to-b from-blue-600 via-blue-400 to-zinc-200" initial={{ scaleY: 0 }} whileInView={{ scaleY: 1 }} viewport={{ once: true, amount: 0.3 }} transition={{ duration: 1.4, ease: [0.22, 1, 0.36, 1] }} /> {items.map((step, i) => ( <motion.li key={step.title} className="relative flex gap-6 pb-10 last:pb-0" initial={{ opacity: 0, x: -16 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true, amount: 0.5 }} transition={{ duration: 0.6, delay: 0.15 + i * 0.15, ease: [0.22, 1, 0.36, 1] }} > <span className="relative z-10 grid size-10 shrink-0 place-items-center rounded-full border border-zinc-200 bg-white text-sm font-semibold text-zinc-900 shadow-sm"> {String(i + 1).padStart(2, "0")} </span> <div className="pt-1.5"> <div className="flex flex-wrap items-center gap-2"> <h3 className="font-semibold text-zinc-900">{step.title}</h3> {step.tag && <span className="rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500">{step.tag}</span>} </div> <p className="mt-1.5 max-w-md text-sm leading-relaxed text-zinc-500">{step.text}</p> </div> </motion.li> ))} </ol> ); } export default function Demo() { return ( <div className="w-full max-w-xl"> <p className="text-sm font-medium text-blue-600">How it works</p> <h2 className="mb-10 mt-2 text-3xl font-semibold tracking-tight text-zinc-900">From setup to insight in minutes</h2> <ProcessTimeline /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.