A feature section that walks a release through its five stations as tabs that play by themselves. The LED dots under the current station count up like a timer before the next one comes up; point at a station and the lit run jumps under it, pick one and it stays. Each station shows what happens there, with the product's own readout beside it.
registry/blocks/dot-rail-feature.tsx
"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { DotRail } from "@/components/ui/dot-rail";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the section yours: the
* palette, the copy and the stations. The stations play by
* themselves: the dots under the current one count up like a
* timer, then the next one comes up. Point at a station and
* the rail jumps under it; pick one and it stays.
* ─────────────────────────────────────────────────────────── */
/** Paper and ink, an electric blue for the headline's italic, and a hot signal color for the rail's lit dots. */
const PALETTE = {
light: { paper: "#fafaf9", ink: "#0b0b0d", accent: "#2747ff", signal: "#ff5a1f", platform: "#ffffff" },
dark: { paper: "#07080c", ink: "#f4f4f6", accent: "#8292ff", signal: "#ff6a2b", platform: "#10121a" },
};
const HEADLINE = { before: "Watch a release", accent: "ride", after: "the line" };
const BODY =
"Every Tuesday's train calls at the same five stations. Pick one to see what Milepost does there, or let it run.";
const TABS_LABEL = "Release stations";
/** ms each station stays up while the section plays by itself. */
const DWELL = 4500;
const TICK = 80;
type Row = { name: string; value: string; state: "done" | "live" | "wait" };
type Station = { id: string; label: string; time: string; title: string; body: string; rows: Row[] };
const STATIONS: Station[] = [
{
id: "cut",
label: "Branch cut",
time: "Tue 10:00",
title: "The doors close on time",
body: "At ten the release branch is cut from main. Anything merged after rides next week, and nobody has to ask.",
rows: [
{ name: "release/214", value: "cut from main at 4f1c9e2", state: "done" },
{ name: "Aboard", value: "38 changes from 4 teams", state: "done" },
{ name: "Next cut", value: "Tue 10:00", state: "wait" },
],
},
{
id: "checks",
label: "Checks",
time: "10:04",
title: "Every change shows its ticket",
body: "Tests, migrations and the bundle budget run on the branch itself, so a red check names the change that broke it.",
rows: [
{ name: "Unit", value: "2,914 passed", state: "done" },
{ name: "End to end", value: "186 passed", state: "done" },
{ name: "Migrations", value: "2, both reversible", state: "done" },
],
},
{
id: "staging",
label: "Staging",
time: "10:31",
title: "A dress rehearsal with real data",
body: "The train stops on staging for an hour. The teams who own the changes get a link and a checklist, not a meeting.",
rows: [
{ name: "Payments", value: "signed off", state: "done" },
{ name: "Search", value: "signed off", state: "done" },
{ name: "Mobile", value: "checking now", state: "live" },
],
},
{
id: "canary",
label: "10% of users",
time: "11:15",
title: "A canary that can stop the train",
body: "One in ten requests rides the new release. If errors or latency drift past your limits, it rolls back by itself.",
rows: [
{ name: "Error rate", value: "0.21%, limit 0.5%", state: "done" },
{ name: "p95 latency", value: "184 ms, limit 250 ms", state: "done" },
{ name: "Auto-rollback", value: "armed", state: "live" },
],
},
{
id: "everyone",
label: "Everyone",
time: "13:00",
title: "Arrived, with the notes written",
body: "The release reaches everyone by lunch, and the changelog is drafted from the tickets that rode it.",
rows: [
{ name: "Rollout", value: "100%", state: "done" },
{ name: "Changelog", value: "drafted, 38 entries", state: "done" },
{ name: "Train 215", value: "boarding", state: "wait" },
],
},
];
/* ── Section ─────────────────────────────────────────────── */
export function DotRailFeature() {
const sectionRef = React.useRef<HTMLElement>(null);
const tabsRef = React.useRef<HTMLDivElement>(null);
const inView = useInView(sectionRef, { amount: 0.35 });
const reduced = useReducedMotion();
const [clock, setClock] = React.useState({ active: 0, elapsed: 0 });
const [auto, setAuto] = React.useState(true);
const [paused, setPaused] = React.useState(false);
const playing = auto && !paused && inView && !reduced;
React.useEffect(() => {
if (!playing) return;
const id = window.setInterval(() => {
setClock(({ active, elapsed }) =>
elapsed + TICK >= DWELL
? { active: (active + 1) % STATIONS.length, elapsed: 0 }
: { active, elapsed: elapsed + TICK },
);
}, TICK);
return () => window.clearInterval(id);
}, [playing]);
const pick = (index: number) => {
setAuto(false);
setClock({ active: index, elapsed: 0 });
};
const onKey = (e: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
const n = STATIONS.length;
const to =
e.key === "ArrowRight" ? (index + 1) % n : e.key === "ArrowLeft" ? (index - 1 + n) % n : e.key === "Home" ? 0 : e.key === "End" ? n - 1 : -1;
if (to < 0) return;
e.preventDefault();
pick(to);
tabsRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]')[to]?.focus();
};
const { active, elapsed } = clock;
return (
<section
ref={sectionRef}
style={
{
"--paper-light": PALETTE.light.paper,
"--ink-light": PALETTE.light.ink,
"--accent-light": PALETTE.light.accent,
"--signal-light": PALETTE.light.signal,
"--platform-light": PALETTE.light.platform,
"--paper-dark": PALETTE.dark.paper,
"--ink-dark": PALETTE.dark.ink,
"--accent-dark": PALETTE.dark.accent,
"--signal-dark": PALETTE.dark.signal,
"--platform-dark": PALETTE.dark.platform,
} as React.CSSProperties
}
className="bg-(--paper) text-(--ink) [--accent:var(--accent-light)] [--dot-rail-accent:var(--signal)] [--ink:var(--ink-light)] [--paper:var(--paper-light)] [--platform:var(--platform-light)] [--signal:var(--signal-light)] dark:[--accent:var(--accent-dark)] dark:[--ink:var(--ink-dark)] dark:[--paper:var(--paper-dark)] dark:[--platform:var(--platform-dark)] dark:[--signal:var(--signal-dark)]"
>
<div className="mx-auto max-w-6xl px-6 py-24 sm:py-28">
<div className="grid gap-5 md:grid-cols-[minmax(0,1fr)_minmax(0,24rem)] md:items-end md:gap-12">
<h2 className="font-serif text-4xl leading-[1.05] font-normal tracking-[-0.02em] text-balance sm:text-5xl">
{HEADLINE.before} <em className="text-(--accent)">{HEADLINE.accent}</em> {HEADLINE.after}
</h2>
<p className="text-base leading-relaxed text-pretty opacity-70">{BODY}</p>
</div>
<div
className="mt-14"
onPointerEnter={(e) => e.pointerType === "mouse" && setPaused(true)}
onPointerLeave={(e) => e.pointerType === "mouse" && setPaused(false)}
>
<div ref={tabsRef} role="tablist" aria-label={TABS_LABEL} className="grid grid-cols-5">
{STATIONS.map((station, index) => (
<button
key={station.id}
id={`milepost-tab-${station.id}`}
type="button"
role="tab"
aria-selected={index === active}
aria-controls={`milepost-panel-${station.id}`}
tabIndex={index === active ? 0 : -1}
onClick={() => pick(index)}
onKeyDown={(e) => onKey(e, index)}
className="group rounded-md px-1 pb-3 text-left outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--accent)"
>
<span className="hidden font-mono text-[11px] tracking-[0.1em] uppercase tabular-nums opacity-45 sm:block">
{station.time}
</span>
<span className="block text-[13px] leading-tight font-medium sm:mt-1 opacity-45 transition-opacity duration-150 group-hover:opacity-100 group-aria-selected:opacity-100 sm:text-[15px]">
{station.label}
</span>
</button>
))}
</div>
<DotRail trackRef={tabsRef} fill={auto && !reduced ? elapsed / DWELL : 1} />
<div className="mt-6 grid">
{STATIONS.map((station, index) => (
<div
key={station.id}
id={`milepost-panel-${station.id}`}
role="tabpanel"
aria-labelledby={`milepost-tab-${station.id}`}
inert={index !== active}
className={cn(
"col-start-1 row-start-1 rounded-2xl bg-(--platform) p-6 ring-1 ring-current/10 transition-[opacity,translate] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:translate-y-0 sm:p-8",
index === active ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-1.5 opacity-0",
)}
>
<Platform station={station} />
</div>
))}
</div>
</div>
</div>
</section>
);
}
/* ── Platform ────────────────────────────────────────────── */
/** What happens at one station: what it does, and a strip of the product's own readout. */
function Platform({ station }: { station: Station }) {
return (
<div className="grid gap-6 md:grid-cols-[minmax(0,1fr)_minmax(0,22rem)] md:items-center md:gap-10">
<div>
<h3 className="font-serif text-2xl leading-tight tracking-[-0.01em]">{station.title}</h3>
<p className="mt-2 max-w-md text-[15px] leading-relaxed text-pretty opacity-70">{station.body}</p>
</div>
<ul className="divide-y divide-current/10 rounded-xl px-4 ring-1 ring-current/10">
{station.rows.map((row) => (
<li key={row.name} className="flex items-center gap-3 py-2.5 font-mono text-[12px]">
<span
aria-hidden
className={
row.state === "done"
? "size-1.5 rounded-full bg-current"
: row.state === "live"
? "size-1.5 rounded-full bg-(--signal)"
: "size-1.5 rounded-full ring-1 ring-current/40"
}
/>
<span className="opacity-60">{row.name}</span>
<span className="ml-auto text-right tabular-nums">{row.value}</span>
</li>
))}
</ul>
</div>
);
}
Live preview and the components it's built with: https://keyframeui.com/blocks/feature-sections/dot-rail-feature