Retro Back Link
A single "go back" anchor styled for retro / terminal chrome. Renders either as a bordered pill (the default — drops cleanly into a header rail or a top breadcrumb) or as a plain inline link (slots into body copy). On pointer enter or keyboard focus the visible label can swap to an alternate hover affordance via a stacked opacity crossfade, while the accessible name stays stable. The whole thing slides in from the left on first paint with a spring.
Customize
Layout
pill
Slots
Installation
npx shadcn@latest add https://craftbits.dev/r/retro-back-link.jsonUsage
import { RetroBackLink } from "@craft-bits/core";
<RetroBackLink href="/archive" label="Back" />Swap the label on hover / focus without changing the announced name:
<RetroBackLink
href="/archive"
label="Back"
hoverLabel="Back to archive"
/>Use the inline variant for in-prose links:
<RetroBackLink variant="inline" href="/archive" label="back to archive" />Pass a custom glyph for the caret or hide it entirely with caret={null}. Drop a small badge, SVG, or counter into the trailing decor slot.
Understanding the component
- Anchor-first. Renders a real anchor element so right-click, middle-click, and keyboard activation all work without router coupling.
- Caret is its own slot. The leading glyph defaults to a left arrow and is rendered in its own
aria-hiddenspan.caret={null}hides it; any node replaces it. - Hover-label stacks underneath. Instead of mutating the DOM, the resting label and
hoverLabelare stacked absolutely inside a relative wrapper. Opacity crossfades on:hoverand:focus-visibleswap them.aria-labelkeeps the accessible name pinned to the resting label so assistive tech does not see the change. - Spring slide-in. First paint slides from
x: -6to0with theSPRINGS.smoothtoken.prefers-reduced-motion: reduceand thedisableMotionprop both short-circuit to a static wrapper. - Two visual variants.
pill(default) draws a bordered pill on the elevated surface and pairs with header rails.inlinestrips the border and surface, leaving a plain text link for body copy. - Trailing decor slot. Renders after the label inside the same anchor, separated by the default gap. Hidden from assistive tech so the announced name stays the label alone.
- Focus and hover share state. The opacity crossfade fires on both
:hoverand:focus-visible, so keyboard-only users see the same affordance without a pointer. - Data hooks. The anchor carries
data-cb-retro-back-linkanddata-variant; the caret, label, and decor each carry a matching attribute for consumer CSS targeting.
Variants
Pill (default)
<RetroBackLink href="/archive" label="Back" />Inline
<RetroBackLink variant="inline" href="/archive" label="back to archive" />With hover-label swap
<RetroBackLink
href="/archive"
label="Back"
hoverLabel="Back to archive"
/>Without caret
<RetroBackLink href="/archive" caret={null} label="Back" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | '/' | Destination URL or app-relative path. |
label | ReactNode | 'Back' | Resting label content. String labels are used as the accessible name. |
hoverLabel | ReactNode | — | When set, crossfaded in on pointer enter / focus and out on leave / blur. |
caret | ReactNode | left arrow | Leading glyph. Pass null to hide. |
decor | ReactNode | — | Trailing decorative slot rendered after the label. Hidden from assistive tech. |
variant | 'pill' | 'inline' | 'pill' | Visual style. pill is bordered; inline is a plain text link. |
disableMotion | boolean | false | Skip the slide-in mount animation. |
className | string | — | Merged onto the anchor via cn(). |
...rest | AnchorHTMLAttributes<HTMLAnchorElement> | — | Any other anchor attribute. |
Accessibility
- Renders a real anchor so default link semantics, keyboard activation, and right-click context menus all work.
- A
:focus-visiblering tied to the accent token gives keyboard-only users a clear focus indicator across both variants. - The
aria-labelis set to the string form oflabelwhen available, so the announced name stays stable even while the visible text crossfades tohoverLabel. - The hover-label crossfade fires on
:focus-visible, so keyboard users see the longer affordance without a pointer. - The
caret,hoverLabel, anddecorslots arearia-hidden="true"— assistive tech reads only the resting label. - The spring slide-in respects
prefers-reduced-motion: reduceand thedisableMotionprop, both short-circuiting to a static wrapper. - Color contrast on
--cb-fg-mutedand--cb-accentmeets WCAG AA against both--cb-bgand--cb-bg-elevated.
Credits
- Extracted from:
terminal-dreams(src/components/retro/RetroBackLink.tsx). The original was anext/link-bound component with two hard-coded variants, inline-style borders, a hard-codedBack/Back to archiveswap that mutatedtextContenton mouse events, and a hand-rolledusePrefersReducedMotion()hook. craft-bits keeps the variant matrix, the slide-in, and the hover-swap behaviour, but generalises every piece: a plain anchor instead ofnext/linkso consumers wire their own router, CVA-driven Tailwind classes instead of inline styles, configurablelabel/hoverLabel/caret/decorprops instead of hard-coded strings, a stacked opacity crossfade instead of DOM mutation so the accessible name stays stable, focus-aware swap so keyboard users get the same affordance, anduseReducedMotion()frommotion/reactfor the reduced-motion fallback.