Retro Timeline

A single-rail vertical timeline for changelogs, archives, CVs, and build logs. Renders a list of { id, date, title, description? } events along a 1px rail with a small accent dot pinned to each title. Pure layout — every chrome region is an optional ReactNode, the only data the rail consumes is an events[] array, and routing is delegated to an optional renderEventHref resolver. Generalised from the original retro shell where series / part grouping, month bucketing, and Next.js routing were baked in.

/changelog
Changelog

Milestones from the build log — newest first.

  1. v3 shipped — retro shell becomes a primitive

    Extracted the timeline rail into a slot-based layout. No more project-specific routing.

  2. Design tokens unified across the docs site

    Every retro component now reads --cb-* tokens. Themability landed without a fork.

  3. First prototype of the archive timeline

    Sketched a single-rail vertical layout with month buckets and inline badges.

  4. Project genesis

Customize
Style
cozy
short
plain
Regions

Installation

npx shadcn@latest add https://craftbits.dev/r/retro-timeline.json

Usage

Pass an array of event objects and let the rail render the timeline:

import { RetroTimeline } from "@craft-bits/core";
 
const events = [
  { id: "v3", date: "2026-05-22", title: "v3 shipped" },
  { id: "v2", date: "2026-05-08", title: "Design tokens unified" },
];
 
<RetroTimeline events={events} />;

Wire it into your router by passing a renderEventHref resolver — each row becomes a focusable anchor:

<RetroTimeline
  events={events}
  renderEventHref={(event) => "/log/" + event.id}
/>;

Drop content into the optional header, intro, footer, and eyebrow slots without forking the shell:

<RetroTimeline
  events={events}
  header={<MyMasthead />}
  intro={<ArchiveBlurb />}
  footer={<SiteFooter />}
  eyebrow="/changelog"
/>;

Understanding the component

  1. Pure layout, no controllers. The rail takes ReactNode slot props and an events[] array and arranges them. No data fetching, no router coupling, no scroll listeners. The original RetroTimeline baked in groupByMonth, series / part folding, and hardcoded Next.js routes; here all of that is the consumer's call.
  2. Rail + dots via pseudo-element. The ordered list carries the rail as a ::before pseudo-element so no extra wrapper div is needed. Each row pins a small accent dot to the title line via absolute positioning so the rail reads as continuous even at the compact density.
  3. Plain rows or anchor rows. When renderEventHref is omitted, every row renders as a div — read-only timeline. When supplied, rows wrap in RetroTimelineEventLink anchors with hover and :focus-visible feedback against --cb-bg-muted.
  4. Tabular numerals on the date. Dates render as <time> with font-variant-numeric: tabular-nums so vertical columns line up when the rail is dense.
  5. Three date formats. dateFormat accepts long (e.g. May 30, 2026), short (ISO date 2026-05-30, the default), or iso (the raw ISO string). All three are deterministic in UTC so SSR and the client never diverge.
  6. Two density presets. cozy (default) leaves vertical space for a one-line description and reads as an editorial archive; compact tightens row padding and rail gap to a dense changelog.
  7. Eyebrow as an optional left-rail label. Defaults to /archive — timeline (matching the original retro shell). Pass null to suppress, or any ReactNode to override.
  8. Data-attributes for nested styling. The root carries data-cb-retro-timeline, data-surface, data-density, and data-align; every region tags itself with data-cb-retro-timeline-region so a single global rule can re-skin every instance.

Props

RetroTimeline

PropTypeDefaultDescription
eventsReadonlyArray<RetroTimelineEvent>The event list rendered along the rail. Newest-first is conventional.
headerReactNodeHeader pinned above the body. Wordmark + nav, banner, search bar.
introReactNodeOptional intro block rendered between the eyebrow and the rail.
footerReactNodeOptional footer row pinned beneath the rail.
eyebrowReactNode | null'/archive — timeline'Left-rail label. Pass null to suppress.
surface'plain' | 'muted''plain'Page background — --cb-bg or --cb-bg-muted.
density'cozy' | 'compact''cozy'Vertical rhythm between events.
dateFormat'long' | 'short' | 'iso''short'Date label style. All three are UTC-deterministic for SSR safety.
align'left' | 'split''left'Rail layout hint exposed as data-align for downstream theming.
renderEventHref(event: RetroTimelineEvent) => stringWhen supplied, rows render as anchors pointing at the returned href.
listAriaLabelstring'Timeline'aria-label for the rail region.
classNamestringMerged onto the rendered root via cn().
...restOmit<HTMLAttributes<HTMLDivElement>, 'title'>Any other div attribute.

RetroTimelineEvent

FieldTypeRequiredDescription
idstringyesStable unique id used for React keys and the default href fallback.
datestring | numberyesEvent date — parseable string or epoch milliseconds.
titlestringyesDisplay title shown as the row's primary label.
descriptionstringnoOptional one-line summary rendered under the title.

Accessibility

  • The rail is rendered as an ordered list with aria-label="Timeline" (override via listAriaLabel). Each event is a list item so screen readers announce position-in-set.
  • The accent dot on each row is aria-hidden="true" so it stays visual-only and never gets read out.
  • Every anchored row has visible :focus-visible styling — a ring in --cb-accent plus a background lift to --cb-bg-muted.
  • The date is wrapped in <time> with a dateTime attribute set to the ISO representation so AT and search engines read the structured value.
  • No motion — the rail never animates layout. Hover and focus transitions stay under 200ms and are colour-only, so reduced-motion preferences are respected implicitly.
  • Color contrast: every default surface paints --cb-bg / --cb-bg-muted with --cb-fg, --cb-fg-muted, and --cb-fg-subtle text. All three pass WCAG AA against both surfaces.

Credits

  • Extracted from: terminal-dreams (src/components/retro/RetroTimeline.tsx). The original was bound to a TimelinePost shape with project-specific concerns baked in: kind: "post" | "recipe", series / part grouping that re-folded items into nested cards, groupByMonth bucketing, hardcoded Next.js routes, a CSS module of [var(--color-*)] tokens, and a static /archive — timeline left rail. craft-bits widens the API to a pure vertical-rail layout: events[]={ id, date, title, description? }, CVA variants for surface / density / dateFormat / align, header / intro / footer / eyebrow slots, and a renderEventHref escape hatch for routing.