Public Roadmap Columns
Shipped / in progress / planned in three columns, with status carried by icon as well as colour.
A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.
components/changelog-timeline.tsxnpx hoverlab add changelog-timeline
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Every release, in plain language.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
/**
* <ChangelogTimeline> — a dated release list on a vertical rail.
*
* The rail is a `border-s` on the list with dots absolutely positioned over
* it, rather than a separate spine element. That way the line ends exactly
* where the last entry does instead of running past it, and it stays
* aligned when an entry's height changes.
*
* Dates use <time datetime> so the machine-readable value is the ISO
* string, whatever human format the label is written in.
*/
import * as React from 'react'
export type ReleaseKind = 'added' | 'changed' | 'fixed'
export interface ReleaseEntry {
/** ISO date — `2026-07-14`. */
date: string
version: string
title: string
items: { kind: ReleaseKind; text: string }[]
}
export interface ChangelogTimelineProps {
entries?: ReleaseEntry[]
heading?: string
subheading?: string
className?: string
}
const KIND_STYLE: Record<ReleaseKind, string> = {
added: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400',
changed: 'bg-sky-500/15 text-sky-600 dark:text-sky-400',
fixed: 'bg-amber-500/15 text-amber-600 dark:text-amber-400',
}
const DEFAULT_ENTRIES: ReleaseEntry[] = [
{
date: '2026-07-14',
version: 'v2.4.0',
title: 'Dashboard blocks',
items: [
{ kind: 'added', text: 'Twelve dashboard and data-table sections' },
{ kind: 'added', text: 'Keyboard navigation across every table block' },
{ kind: 'changed', text: 'Sidebar collapses below 1024px instead of 768px' },
],
},
{
date: '2026-06-02',
version: 'v2.3.1',
title: 'Accessibility pass',
items: [
{ kind: 'fixed', text: 'Focus ring clipped inside overflow containers' },
{ kind: 'fixed', text: 'Marquee ignored prefers-reduced-motion' },
],
},
{
date: '2026-05-19',
version: 'v2.3.0',
title: 'Theming',
items: [
{ kind: 'added', text: 'Token generator with contrast validation' },
{ kind: 'changed', text: 'All blocks now read semantic colour tokens' },
],
},
]
export function ChangelogTimeline({
entries = DEFAULT_ENTRIES,
heading = 'What shipped recently',
subheading = 'Every release, in plain language.',
className = '',
}: ChangelogTimelineProps) {
return (
<section className={`mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 ${className}`}>
<div className="mx-auto mb-12 max-w-2xl text-center">
<h2 className="text-balance text-3xl font-bold tracking-tight sm:text-4xl">
{heading}
</h2>
{subheading ? <p className="mt-3 text-muted-foreground">{subheading}</p> : null}
</div>
{/*
Rows are keyed on date AND version, not on version alone.
A product changelog has unique version strings, so the version looked
like a natural key — but this block is also the right shape for an
incident history, where the badge is a status word and "Resolved"
appears on every second entry. React then drops the duplicates with a
console warning and the timeline silently loses rows.
*/}
<ol className="relative ms-3 space-y-10 border-s border-border/60 ps-8">
{entries.map((entry) => (
<li key={`${entry.date}-${entry.version}`} className="relative">
<span
aria-hidden
className="absolute -start-[2.3rem] top-1.5 h-3 w-3 rounded-full border-2 border-background bg-primary ring-4 ring-primary/15"
/>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
{entry.title ? (
<h3 data-stress-ignore className="text-lg font-bold tracking-tight">{entry.title}</h3>
) : null}
<span className="rounded-md bg-muted px-2 py-0.5 font-mono text-xs font-semibold text-muted-foreground">
{entry.version}
</span>
<time
dateTime={entry.date}
className="text-xs text-muted-foreground"
>
{formatDate(entry.date)}
</time>
</div>
<ul className="mt-4 space-y-2">
{entry.items.map((item) => (
<li key={item.text} className="flex items-start gap-2.5 text-sm">
<span
className={`mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide ${KIND_STYLE[item.kind]}`}
>
{item.kind}
</span>
<span className="text-muted-foreground">{item.text}</span>
</li>
))}
</ul>
</li>
))}
</ol>
</section>
)
}
function formatDate(iso: string): string {
// Parsed as UTC and formatted as UTC — a bare `new Date('2026-07-14')` is
// midnight UTC, which renders as the previous day for anyone west of it.
return new Date(`${iso}T00:00:00Z`).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
})
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/changelog-timeline.tsx and import it where you need the section:
import { ChangelogTimeline } from '@/components/changelog-timeline'2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
entries | ReleaseEntry[] | DEFAULT_ENTRIES |
heading | string | 'What shipped recently' |
subheading | string | 'Every release, in plain language.' |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
<!--
Release Timeline — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block has no interactive behaviour, so nothing is missing.
-->
<section class="mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 ">
<div class="mx-auto mb-12 max-w-2xl text-center">
<h2 class="text-balance text-3xl font-bold tracking-tight sm:text-4xl">What shipped recently</h2>
<p class="mt-3 text-muted-foreground">Every release, in plain language.</p>
</div>
<ol class="relative ms-3 space-y-10 border-s border-border/60 ps-8">
<li class="relative">
<span aria-hidden="true" class="absolute -start-[2.3rem] top-1.5 h-3 w-3 rounded-full border-2 border-background bg-primary ring-4 ring-primary/15"></span>
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 data-stress-ignore="true" class="text-lg font-bold tracking-tight">Dashboard blocks</h3>
<span class="rounded-md bg-muted px-2 py-0.5 font-mono text-xs font-semibold text-muted-foreground">v2.4.0</span>
<time dateTime="2026-07-14" class="text-xs text-muted-foreground">Jul 14, 2026</time>
</div>
<ul class="mt-4 space-y-2">
<li class="flex items-start gap-2.5 text-sm">
<span class="mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">added</span>
<span class="text-muted-foreground">Twelve dashboard and data-table sections</span>
</li>
<li class="flex items-start gap-2.5 text-sm">
<span class="mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">added</span>
<span class="text-muted-foreground">Keyboard navigation across every table block</span>
</li>
<li class="flex items-start gap-2.5 text-sm">
<span class="mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide bg-sky-500/15 text-sky-600 dark:text-sky-400">changed</span>
<span class="text-muted-foreground">Sidebar collapses below 1024px instead of 768px</span>
</li>
</ul>
</li>
<li class="relative">
<span aria-hidden="true" class="absolute -start-[2.3rem] top-1.5 h-3 w-3 rounded-full border-2 border-background bg-primary ring-4 ring-primary/15"></span>
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 data-stress-ignore="true" class="text-lg font-bold tracking-tight">Accessibility pass</h3>
<span class="rounded-md bg-muted px-2 py-0.5 font-mono text-xs font-semibold text-muted-foreground">v2.3.1</span>
<time dateTime="2026-06-02" class="text-xs text-muted-foreground">Jun 2, 2026</time>
</div>
<ul class="mt-4 space-y-2">
<li class="flex items-start gap-2.5 text-sm">
<span class="mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide bg-amber-500/15 text-amber-600 dark:text-amber-400">fixed</span>
<span class="text-muted-foreground">Focus ring clipped inside overflow containers</span>
</li>
<li class="flex items-start gap-2.5 text-sm">
<span class="mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide bg-amber-500/15 text-amber-600 dark:text-amber-400">fixed</span>
<span class="text-muted-foreground">Marquee ignored prefers-reduced-motion</span>
</li>
</ul>
</li>
<li class="relative">
<span aria-hidden="true" class="absolute -start-[2.3rem] top-1.5 h-3 w-3 rounded-full border-2 border-background bg-primary ring-4 ring-primary/15"></span>
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 data-stress-ignore="true" class="text-lg font-bold tracking-tight">Theming</h3>
<span class="rounded-md bg-muted px-2 py-0.5 font-mono text-xs font-semibold text-muted-foreground">v2.3.0</span>
<time dateTime="2026-05-19" class="text-xs text-muted-foreground">May 19, 2026</time>
</div>
<ul class="mt-4 space-y-2">
<li class="flex items-start gap-2.5 text-sm">
<span class="mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">added</span>
<span class="text-muted-foreground">Token generator with contrast validation</span>
</li>
<li class="flex items-start gap-2.5 text-sm">
<span class="mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide bg-sky-500/15 text-sky-600 dark:text-sky-400">changed</span>
<span class="text-muted-foreground">All blocks now read semantic colour tokens</span>
</li>
</ul>
</li>
</ol>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Want the whole screen instead of this one section? Open a page and copy it entire.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse Content & BlogCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
Shipped / in progress / planned in three columns, with status carried by icon as well as colour.
A featured post at full width above a card grid, with drawn covers built from tokens — a blog index with no image pipeline and no broken-thumbnail state.
A grid of case-study cards that lead with the metric rather than the company name, each one linking out to the full write-up — the block a customer-stories section or a home page needed and only had a full case-study page for.
Kicker, headline, standfirst, byline and share row, with the opening prose and a pull quote included — the type scale of a post, decided once instead of per-article.
An about-page team section on initials avatars rather than photos, so shipping it never waits on headshots and there is no broken-image state to design.
Openings grouped by department with location, type and salary on every row — candidates self-select by craft first, and a flat list makes them scan for it.
Sidebar, article and on-this-page rail — each column answers a different question, and they collapse on mobile in the order that keeps the article alive.
Method badge, path, parameter table and a request/response pair kept side by side — an example request whose response is a scroll away gets pasted blind.
One install command behind npm/pnpm/yarn/bun tabs with a working copy button — every reader uses exactly one package manager, so showing all four wastes three lines.
A CV as a document rather than a timeline of cards — one column, dates on the end edge, and a print layer that puts it on paper without a separate PDF to keep in step.
The watch screen with no playback in it: a ratio-locked stage, a control bar drawn down to the buffered layer, and an up-next rail. Drop a video element in and wire your own player.