Retro Sidebar

A sidebar navigation for screens that want a terminal or retro CRT feel. Renders one or more labelled sections of links, with optional header, footer, and decor slots — shader canvases, gradients, scanline overlays — without coupling the sidebar to a specific renderer.

Customize
Layout
comfortable
plain
Slots

Installation

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

Usage

RetroSidebar is a section-based nav primitive. Pass a sections array; the component renders the headings, the link lists, and the landmark wrapper.

import { RetroSidebar } from "@craft-bits/core";
 
<RetroSidebar
  sections={[
    {
      label: "Archive",
      items: [
        { label: "~/2026", href: "/archive/2026", current: true },
        { label: "~/2025", href: "/archive/2025" },
      ],
    },
    {
      label: "Series",
      items: [
        { label: "~/principles", href: "/series/principles" },
        { label: "~/playground", href: "/series/playground" },
      ],
    },
  ]}
/>

Pass a background canvas, shader, or gradient into the decor slot — the sidebar positions it absolute, behind the content, with pointer-events: none and aria-hidden:

<RetroSidebar
  sections={sections}
  decor={<ScanlineCanvas />}
/>

Drop a status pill, search box, or sign-off into the header and footer slots — both are freeform ReactNodes rendered above the first and below the last section.

Understanding the component

  1. Section-shaped data. Each section is a label plus a list of items, and each item is a label, href, and optional current flag. The data shape mirrors how sidebars actually compose in practice — group by topic, link by URL, mark the current page.
  2. Aside is a landmark. The root renders an aside with a configurable aria-label (defaults to "Sidebar"), so screen readers announce the complementary region correctly.
  3. Per-section nav. Each section's link list is wrapped in its own nav with aria-label set to the section's label, so assistive tech can jump between groups instead of treating the whole sidebar as one bag of links.
  4. Current page hook. Items with current: true get aria-current="page", a data-current attribute, and the accent color — three independent style and ARIA hooks consumers can target.
  5. Decor is fully consumer-controlled. Terminal shaders, WebGL canvases, gradient overlays — none of them ship in the sidebar. The slot is a positioned wrapper with pointer-events: none and aria-hidden; the consumer supplies the renderer.
  6. Header and footer slots. Optional ReactNode slots for a status pill, search box, theme toggle, or sign-off — composed freely by the caller, no array shape.
  7. Density variants. compact, comfortable, spacious tune the inner padding for tight chrome vs. roomy panels.
  8. Tone variants. plain paints the default background; muted switches to the muted surface token so the sidebar reads as a secondary panel against a primary canvas.
  9. Data hooks. The root carries data-cb-retro-sidebar, data-density, and data-tone; each section carries data-cb-retro-sidebar-section; each link carries data-cb-retro-sidebar-nav-link for consumer styling without prop-drilling.

Variants

Single section

<RetroSidebar
  sections={[
    {
      label: "Archive",
      items: [
        { label: "~/2026", href: "/archive/2026" },
        { label: "~/2025", href: "/archive/2025" },
      ],
    },
  ]}
/>

Multiple sections with current marker

<RetroSidebar
  sections={[
    {
      label: "Archive",
      items: [
        { label: "~/2026", href: "/archive/2026", current: true },
        { label: "~/2025", href: "/archive/2025" },
      ],
    },
    {
      label: "Series",
      items: [
        { label: "~/principles", href: "/series/principles" },
        { label: "~/playground", href: "/series/playground" },
      ],
    },
  ]}
/>

Compact, muted tone

<RetroSidebar
  density="compact"
  tone="muted"
  sections={sections}
/>

With header and footer slots

<RetroSidebar
  sections={sections}
  header={<SearchBox />}
  footer={<span>(c) 2026 — terminal_dreams</span>}
/>

Props

RetroSidebar

PropTypeDefaultDescription
sectionsRetroSidebarSection[]Required. Labelled groups of items rendered as nav sections.
ariaLabelstring"Sidebar"Landmark label on the aside element.
headerReactNodeSlot rendered above the first section.
footerReactNodeSlot rendered below the last section.
decorReactNodeDecorative background slot (positioned absolute, aria-hidden).
density'compact' | 'comfortable' | 'spacious''comfortable'Inner padding scale.
tone'plain' | 'muted''plain'Surface tone.
classNamestringMerged onto the rendered aside via cn().
...restHTMLAttributes<HTMLElement>Any other aside attribute.

RetroSidebarSection

FieldTypeDescription
labelstringSection heading.
itemsRetroSidebarItem[]Items inside the section.

RetroSidebarItem

FieldTypeDescription
labelstringDisplay label for the link.
hrefstringAnchor href.
currentbooleanMark as current page. Adds aria-current="page" and accent styling.

RetroSidebar.NavLink

PropTypeDefaultDescription
hrefstringAnchor href.
itemClassNamestringOverride classes on the wrapping list item.
classNamestringMerged onto the anchor via cn().
...restAnchorHTMLAttributes<HTMLAnchorElement>Any other anchor attribute.

Accessibility

  • The root renders an aside landmark with a configurable aria-label — screen readers announce the complementary region correctly.
  • Each section's list is wrapped in its own nav with aria-label set to the section's label, so assistive tech can jump between groups.
  • Items marked current: true get aria-current="page" so screen readers announce the current location.
  • Links have a :focus-visible ring tied to the accent token; color contrast on --cb-fg, --cb-fg-muted, and --cb-accent meets WCAG AA.
  • The decor slot is aria-hidden="true" with pointer-events: none, so background art never traps focus or screen-reader cursors.

Credits

  • Extracted from: terminal-dreams (src/components/retro/RetroSidebar.tsx). The original was a one-off "system status" widget — a fixed grid of four hard-coded stats bound to a single postsCount number, wrapped in a CSS module tied to terminal-dreams' theme. craft-bits keeps the underlying intent — a retro / terminal sidebar — and lifts it into a generalised section-based navigation primitive: a sections array of labelled link groups, plus header, footer, and decor slots, CVA density / tone variants, cb-* token-driven styling, forwardRef, and className merging via cn(). The status-widget shape is gone; in its place is the canonical sidebar-nav shape consumers actually need.