Webring List

A <nav> landmark for indie webrings — a prev / next sibling strip plus a full directory of every site in the ring. Order of the sites prop is the order of the ring; exactly one entry flagged current: true anchors the siblings and gets the active highlight.

Customize
Shape
4

Installation

npx shadcn@latest add https://craftbits.dev/r/webring-list.json

Usage

Pass an ordered ring of sites. Flag exactly one as current: true — its neighbours become the prev / next siblings shown above the directory. The ring wraps, so the site after the last entry is the first entry, and vice versa.

import { WebringList } from "@craft-bits/core";
 
<WebringList
  sites={[
    { id: "alice", name: "Alice", url: "https://alice.example" },
    { id: "me",    name: "My Site", url: "https://me.example", current: true },
    { id: "bob",   name: "Bob",   url: "https://bob.example" },
  ]}
/>

Render only the compact sibling strip — useful at the foot of an article — by turning the directory off:

<WebringList sites={sites} showDirectory={false} />

Render only the catalog — useful on a standalone "all members" page — by turning the sibling row off:

<WebringList sites={sites} showSiblings={false} />

Understanding the component

  1. Sibling computation. WebringList finds the entry flagged current: true, then picks the previous and next sites in the array — with wrap-around at both ends. The order of the array is the order of the ring, so the consumer controls direction.
  2. Two stacked regions. The <nav> renders a sibling strip on top (prev label, arrow, name; mirror on the right for next) and the directory below. Each region is its own subcomponent, so consumers can render either in isolation via WebringList.Siblings or WebringList.Directory.
  3. Current entry hook. In the directory, the matching <li> gets the cb-accent border and its link carries aria-current="page". Assistive tech announces it as "current," and sighted users see the distinct highlight.
  4. External by default. Every ring link defaults to target="_blank" with rel="noopener noreferrer" — webring members are by definition other people's sites. Override both attributes per call site, or pass asChild to delegate routing to your framework's link primitive.
  5. Empty + single-site states. With no sites, the component renders the emptyState slot (defaulting to a single muted line). With one site, the sibling row collapses to two em-dashes because a ring of one has no siblings.
  6. Focus visible. Each ring link gets a focus-visible: ring keyed to --cb-accent, offset from --cb-bg, so keyboard users always see the current target on every theme surface.

Props

WebringList

PropTypeDefaultDescription
sitesreadonly WebringSite[]Ordered ring of sites. Flag exactly one with current: true to anchor siblings.
aria-labelstring"Webring"Accessible name for the <nav> landmark.
emptyStateReactNodemuted lineRendered when sites is empty.
showSiblingsbooleantrueShow the prev / next row above the directory.
showDirectorybooleantrueShow the full directory list of sites.
prevLabelReactNode"Prev"Label rendered before the previous-sibling link.
nextLabelReactNode"Next"Label rendered before the next-sibling link.
classNamestringMerged onto the rendered <nav>.
...restHTMLAttributes<HTMLElement>Any other <nav> prop.

WebringSite

FieldTypeDescription
idstringStable identifier — used as the React key.
nameReactNodeDisplay name.
urlstringDestination URL. Opened in a new tab by default.
descriptionReactNodeOptional one-line tagline rendered under the name in the directory.
currentbooleanMark the consumer's own entry. Exactly one site should be flagged.

WebringList.Siblings

PropTypeDefaultDescription
prevWebringSite | nullPrevious site in the ring, or null when none.
nextWebringSite | nullNext site in the ring, or null when none.
prevLabelReactNode"Prev"Label before the previous-sibling link.
nextLabelReactNode"Next"Label before the next-sibling link.
classNamestringMerged onto the rendered <div>.

WebringList.Directory

PropTypeDefaultDescription
sitesreadonly WebringSite[]Ordered ring of sites.
currentIndexnumber-1Index of the entry to highlight; -1 for no highlight.
classNamestringMerged onto the rendered <ul>.

WebringList.Link

PropTypeDefaultDescription
asChildbooleanfalseRender as the child element instead of an <a> (Radix Slot).
targetstring"_blank"Anchor target. Override to keep navigation in-tab.
relstring"noopener noreferrer"Anchor rel.
classNamestringMerged onto the rendered element.
...restAnchorHTMLAttributes<HTMLAnchorElement>Any other anchor prop.

Accessibility

  • The root renders <nav aria-label="Webring"> so assistive tech announces the region. Override with the aria-label prop for a more specific name.
  • The current directory entry carries aria-current="page" and gets a stronger border so both sighted and assistive-tech users know it is the consumer's own entry.
  • Sibling arrows are wrapped in aria-hidden spans because the directional cue is already in the visible "Prev" / "Next" labels and the announced link names.
  • Each sibling link has a richer accessible name — "Previous site: Alice's Garden" — built from aria-label so the announcement carries direction plus destination even when the visible link text is short.
  • Every ring link gets a focus-visible: ring keyed to --cb-accent, offset from --cb-bg, so keyboard users always see the current target on every theme surface.
  • Color contrast in the default theme: directory link text uses --cb-accent against --cb-surface, the current entry uses --cb-fg, and sibling labels use --cb-fg-subtle — all pass WCAG AA.

Credits

  • Extracted from: terminal-dreams (src/components/webring/WebringList.tsx). The original was a flat list — one <a> per site with no anchor to a current entry and no prev/next derivation. craft-bits lifts the API into a <nav> landmark with the current flag on WebringSite, computes ring siblings with wrap-around, splits the rendering into a sibling strip and a directory (each independently toggleable), and adds the WebringList.Link slot so consumers can swap in a framework router via asChild.