Retro Blog

A list-view shell for a personal blog or archive page. Masthead row, optional intro block, a vertical list of post links (date / title / excerpt), and optional sidebar / footer / decorative overlay slots. Pure layout — every chrome region is an optional ReactNode and the only data the shell consumes is a posts[] array of { id, title, date, excerpt? }. Generalised from the original retro shell into something that fits a devlog, a changelog, a small-internet zine, or an editorial archive.

Customize
Style
cozy
long
plain
Regions

Installation

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

Usage

Pass an array of post objects and let the shell render the list:

import { RetroBlog } from "@craft-bits/core";
 
const posts = [
  { id: "scanlines", title: "Scanlines and CRT veils", date: "2026-05-22" },
  { id: "mono", title: "Monospace rhythm", date: "2026-05-08" },
];
 
<RetroBlog posts={posts} />;

Wire it into your router by passing a renderPostHref resolver:

<RetroBlog
  posts={posts}
  renderPostHref={(post) => `/posts/${post.id}`}
/>;

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

<RetroBlog
  posts={posts}
  header={<MyMasthead />}
  intro={<ArchiveBlurb />}
  sidebar={<AboutCard />}
  footer={<SiteFooter />}
  decor={<ScanlineOverlay />}
  renderPostHref={(post) => `/posts/${post.id}`}
/>;

Understanding the component

  1. Pure slots, no controllers. The shell takes ReactNode props and a posts[] array and arranges them. No data fetching, no router coupling, no scroll listeners. The original retro shell mounted RetroHeader, RetroTimeline, CraftSection, RetroSidebar, RetroFooter, and RetroDecor directly; here all of that is the consumer's call via slot props.
  2. Two-column body with a drawer fallback. The body row splits into a flex-1 main column and an optional sidebar at the lg: breakpoint. Below lg: the sidebar drops beneath the list and stretches full-width, matching the original cookbook drawer behaviour.
  3. Decor sits in an aria-hidden overlay. The decor slot is wrapped in an absolutely-positioned, pointer-events: none container above the page background but behind the content. Screen readers skip it; clicks pass through to the list.
  4. Post rows are pure anchors. Each post is rendered through RetroBlogPostLink — a single anchor with a stacked date / title / excerpt layout. Hover and :focus-visible swap the row background to --cb-bg-muted and bump the title to --cb-accent for a strong scan signal.
  5. Tabular numerals on the date. Dates render as <time> with font-variant-numeric: tabular-nums so vertical columns line up when the list is dense.
  6. Three date formats. dateFormat accepts long (default — e.g. May 30, 2026), short (ISO date 2026-05-30), or iso (the raw ISO string). All three are deterministic in UTC so SSR and the client never diverge.
  7. Two density presets. cozy (default) leaves vertical space for a one-line excerpt and reads as an editorial archive; compact tightens row padding and gap to a dense changelog feel.
  8. Data-attributes for nested styling. The root carries data-cb-retro-blog, data-surface, data-density, and data-sidebar; every region tags itself with data-cb-retro-blog-region so a single global rule can re-skin every instance.

Props

RetroBlog

PropTypeDefaultDescription
postsReadonlyArray<RetroBlogPost>The post list rendered in the central column. Each entry needs id, title, and date; excerpt is optional.
headerReactNodeMasthead row pinned above the body. Wordmark + nav, banner, search bar.
introReactNodeOptional intro block rendered above the list — section heading + blurb.
sidebarReactNodeOptional aside beside the list at lg:. Drops below the list on narrow viewports.
footerReactNodeOptional footer row pinned beneath the body.
decorReactNodeDecorative overlay (scanlines, CRT veil, cursor glow). Wrapped in aria-hidden, pointer-events: none.
surface'plain' | 'muted''plain'Page background — --cb-bg or --cb-bg-muted.
density'cozy' | 'compact''cozy'Vertical rhythm between list items.
dateFormat'long' | 'short' | 'iso''long'Date label style. All three are UTC-deterministic for SSR safety.
renderPostHref(post: RetroBlogPost) => stringhash fallbackResolves the link href for each post. Defaults to a hash fragment.
listAriaLabelstring'Posts'aria-label for the post list region.
classNamestringMerged onto the rendered root via cn().
...restOmit<HTMLAttributes<HTMLDivElement>, 'title'>Any other div attribute.

RetroBlogPost

FieldTypeRequiredDescription
idstringyesStable unique id used for React keys and the default href fallback.
titlestringyesDisplay title shown as the link label.
datestring | numberyesPublication date — parseable string or epoch milliseconds.
excerptstringnoOptional one-line summary rendered under the title.

Accessibility

  • The root renders a div, the main column is wrapped in a <main>, and the sidebar is wrapped in an <aside> — so assistive tech announces the right landmarks without the consumer having to re-wrap.
  • The post list is an ordered list (<ol>) with aria-label="Posts" (override via listAriaLabel). Each item is an <li> so screen readers announce position-in-set.
  • Every post link 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.
  • The decor slot is wrapped in aria-hidden="true" with pointer-events: none so decorative overlays never interrupt the reading order or block clicks.
  • No motion — the shell 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/RetroBlog.tsx). The original was a fixed composition of project-specific parts (RetroHeader, RetroTimeline, CraftSection, RetroSidebar, RetroFooter, RetroDecor) all bound to a TimelinePost shape with series grouping, month bucketing, Next.js routing, and a useRetroEffects / usePrefersReducedMotion mount step baked in. craft-bits keeps the masthead + list + sidebar + footer reading flow but reshapes the API into a fully slotted layout: every chrome region is an optional ReactNode, the list reads a minimum posts[]={id,title,date,excerpt?} shape, project routing is delegated to a renderPostHref callback, and every hardcoded [var(--color-bg)] token is rewired onto the --cb-* palette.