Table of Contents Spine
A sticky right rail that follows the article: click an entry to glide to its section, and the entry highlights as you scroll. The docs-page wayfinder that never loses your place.
Every surface on this page is built from the same ten tokens, so nothing can drift out of the system.
Motion here is a language with a tiny vocabulary: enter, focus, state. Nothing decorates for its own sake.
We treat reduced motion as a second design, not a deletion — the story survives without the theatre.
If a component can't be reached with a keyboard and understood by a screen reader, it doesn't ship.
The library grows weekly and the changelog says exactly what changed and why — no vague release notes.
Ambience is a live light wash over the preview. Full palette re-theming of every demo arrives with Theme Studio (Pro).
// Table of Contents Spine — the demo's own source, extracted verbatim.
// From src/components/demos/scenes/set-03.tsx, export TocSpine.
// Needs React 18+ and Tailwind CSS v4. The classes below include this site's
// token utilities (bg-panel, text-ink-dim, chip …); their values are served as
// CSS custom properties at /api/exports/tokens.json. The reduced-motion branch
// is part of the source, not an afterthought.
// 5 helpers the demo imports are inlined above it, verbatim.
import { useEffect, useRef, useState } from "react";
export interface SceneMotion {
/** True when the reader asked the system for less motion. */
reduced: boolean;
/** A duration in milliseconds, collapsed to 0 when reduced. */
ms: (value: number) => number;
/** A repeat count, collapsed to 1 when reduced — an entrance plays once. */
times: (value: number) => number;
}
export function useReducedMotion(): boolean {
const [reduced, setReduced] = useState<boolean>(() =>
typeof window !== "undefined" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false
);
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
const on = (e: MediaQueryListEvent) => setReduced(e.matches);
mq.addEventListener("change", on);
return () => mq.removeEventListener("change", on);
}, []);
return reduced;
}
export function useSceneMotion(): SceneMotion {
const reduced = useReducedMotion();
return {
reduced,
ms: (value: number) => (reduced ? 0 : value),
times: (value: number) => (reduced ? 1 : value),
};
}
const TOC_SECTIONS = [
{ id: "s-intro", t: "Introduction", fill: 0 },
{ id: "s-tokens", t: "Tokens & theming", fill: 1 },
{ id: "s-motion", t: "Motion language", fill: 2 },
{ id: "s-a11y", t: "Accessibility", fill: 3 },
{ id: "s-faq", t: "FAQ", fill: 4 },
];
const TOC_PARAS = [
"Every surface on this page is built from the same ten tokens, so nothing can drift out of the system.",
"Motion here is a language with a tiny vocabulary: enter, focus, state. Nothing decorates for its own sake.",
"We treat reduced motion as a second design, not a deletion — the story survives without the theatre.",
"If a component can't be reached with a keyboard and understood by a screen reader, it doesn't ship.",
"The library grows weekly and the changelog says exactly what changed and why — no vague release notes.",
];
export function TocSpine() {
const scroller = useRef<HTMLDivElement>(null);
// #27 — a JS `behavior: "smooth"` overrides the stylesheet's
// `scroll-behavior: auto`, so the jump has to read the preference itself.
const { reduced } = useSceneMotion();
const [active, setActive] = useState("s-intro");
const jump = (id: string) => {
const el = scroller.current;
if (!el) return;
const target = el.querySelector<HTMLElement>(`[data-sec="${id}"]`);
if (target) el.scrollTo({ top: target.offsetTop - el.offsetTop - 8, behavior: reduced ? "auto" : "smooth" });
};
const onScroll = () => {
const el = scroller.current;
if (!el) return;
let cur = TOC_SECTIONS[0].id;
for (const sec of TOC_SECTIONS) {
const node = el.querySelector<HTMLElement>(`[data-sec="${sec.id}"]`);
if (node && node.offsetTop - el.offsetTop - 24 <= el.scrollTop) cur = sec.id;
}
setActive(cur);
};
return (
<div className="flex h-full w-full flex-col bg-[#0a0c13]">
<div className="flex items-center gap-2 border-b border-white/6 bg-[#0d1017]/95 px-4 py-2">
<span className="text-[10px] font-bold uppercase tracking-[0.22em] text-violet-300/70">Guide · scroll the article</span>
<span className="ml-auto font-mono text-[10px] text-ink-faint">TOC follows you</span>
</div>
<div className="flex min-h-0 flex-1">
{/* article */}
<div ref={scroller} onScroll={onScroll} className="min-w-0 flex-1 overflow-y-auto px-5 py-4">
<div className="mx-auto max-w-sm space-y-3">
{TOC_SECTIONS.map((sec, i) => (
<div key={sec.id} data-sec={sec.id} className="rounded-xl border border-white/7 bg-white/3 p-3.5">
<div className="text-xs font-extrabold tracking-tight">{sec.t}</div>
<div className="mt-1.5 space-y-2">
<div className="h-1.5 w-full rounded-full bg-white/12" />
<div className="h-1.5 w-11/12 rounded-full bg-white/8" />
<div className="h-1.5 w-4/5 rounded-full bg-white/8" />
</div>
<p className="mt-2.5 text-[10px] leading-relaxed text-ink-dim">{TOC_PARAS[i]}</p>
</div>
))}
</div>
</div>
{/* toc spine */}
<div className="hidden w-32 shrink-0 border-l border-white/6 bg-black/25 p-2.5 sm:block">
<div className="px-1 pb-2 text-[8px] font-bold uppercase tracking-[0.22em] text-ink-faint">On this page</div>
<ol className="space-y-0.5">
{TOC_SECTIONS.map((sec) => (
<li key={sec.id}>
<button
type="button"
onClick={() => jump(sec.id)}
className={`flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left text-[9px] font-semibold transition-colors ${
active === sec.id ? "bg-violet-400/15 text-violet-100" : "text-ink-faint hover:text-ink-dim"
}`}
>
<span className={`h-1 w-1 shrink-0 rounded-full ${active === sec.id ? "bg-violet-300" : "bg-white/20"}`} aria-hidden />
{sec.t}
</button>
</li>
))}
</ol>
</div>
</div>
</div>
);
}
Snippet provenance
lines 1–127 · v1.0.0No changelog entry mentions “Table of Contents Spine”, so the catalog records a single version for it.
Every line above is attributed to the single version this catalog records — we keep no per-line history, so no per-line attribution is invented. See the provenance page for the rule and the gaps.
Review notes · anchored to code lines
Threads attach to a line number in this asset's snippet, so a remark about the focus trap stays next to it.
Loading notes…
The snippet on this page is 127 lines long, so anchors run 1–127. Notes are per-asset and per-browser; a shared review thread is a server feature, and this panel is the local stand-in that proves the anchoring works.
Why it works — Long-form pages fail when readers can't see the shape of the argument; a TOC spine restores the map without leaving the page.
The idea — Scroll-spy should highlight by offset comparison, not IntersectionObserver callbacks per section — one scroll handler, one loop, no observer churn.
When to skip it — Under ~5 headings a TOC is furniture; only ship the rail when the page genuinely has chapters.
Course rail — understand it before you ship it
Live variants
Variant gallery
The same Table of Contents Spine demo re-lit in three tones — no code change, three personalities.
Every surface on this page is built from the same ten tokens, so nothing can drift out of the system.
Motion here is a language with a tiny vocabulary: enter, focus, state. Nothing decorates for its own sake.
We treat reduced motion as a second design, not a deletion — the story survives without the theatre.
If a component can't be reached with a keyboard and understood by a screen reader, it doesn't ship.
The library grows weekly and the changelog says exactly what changed and why — no vague release notes.
Every surface on this page is built from the same ten tokens, so nothing can drift out of the system.
Motion here is a language with a tiny vocabulary: enter, focus, state. Nothing decorates for its own sake.
We treat reduced motion as a second design, not a deletion — the story survives without the theatre.
If a component can't be reached with a keyboard and understood by a screen reader, it doesn't ship.
The library grows weekly and the changelog says exactly what changed and why — no vague release notes.
Every surface on this page is built from the same ten tokens, so nothing can drift out of the system.
Motion here is a language with a tiny vocabulary: enter, focus, state. Nothing decorates for its own sake.
We treat reduced motion as a second design, not a deletion — the story survives without the theatre.
If a component can't be reached with a keyboard and understood by a screen reader, it doesn't ship.
The library grows weekly and the changelog says exactly what changed and why — no vague release notes.
Theme Studio preview
Theming example — two token themes
Assets are token-driven (100% design-token driven). Here the same scene runs under two theme presets from the token sheet.
Every surface on this page is built from the same ten tokens, so nothing can drift out of the system.
Motion here is a language with a tiny vocabulary: enter, focus, state. Nothing decorates for its own sake.
We treat reduced motion as a second design, not a deletion — the story survives without the theatre.
If a component can't be reached with a keyboard and understood by a screen reader, it doesn't ship.
The library grows weekly and the changelog says exactly what changed and why — no vague release notes.
--accent 262 · dark surfaces
Every surface on this page is built from the same ten tokens, so nothing can drift out of the system.
Motion here is a language with a tiny vocabulary: enter, focus, state. Nothing decorates for its own sake.
We treat reduced motion as a second design, not a deletion — the story survives without the theatre.
If a component can't be reached with a keyboard and understood by a screen reader, it doesn't ship.
The library grows weekly and the changelog says exactly what changed and why — no vague release notes.
--accent 40 · paper surfaces
90-second read
Usage recipe — where this fits
Put it where a micro-decision happens: a toggle, a loader, a hover state — the small moments that make an interface feel considered.
Drop it into the exact spot it belongs, keep the default props until the copy is final, then tune the one knob that matters for your context.
Cost honesty
Performance note
Cheaper alternative — if you need even lighter, the plain HTML version skips React entirely — same result, ~0 KB.
What it needs
Browser support strip
No polyfills ship with the asset — if a listed feature is missing in a target browser, the graceful fallback is the static layout.
prefers-reduced-motion
Reduced-motion fallback
With prefers-reduced-motion: reduce, the animation loop and travel are removed; state changes keep working through opacity and colour (≤ 300ms). The demo you see above honours the switch — try it in your OS settings and the loop will quiet down.
Design rule: the reduced branch is a second design, not a stripped page — every state change stays visible.
Pressable step-through
Keyboard walk demo
Step through what a keyboard user experiences with this asset.
Tab to the element — the focus ring is visible before interaction
Auto-suggested
Composition map — what pairs well
Same-family assets that compose cleanly with this one:
Prism Switch
A theme-aware toggle that sweeps a six-stop prism gradient across the track on every flip. Pure CSS states, keyboard reachable, announces state to screen readers.
ElementsHalo Button
Primary button with a mouse-tracking halo, press ripple and a springy scale. Ships as React + Tailwind or dependency-free HTML/CSS.
ElementsPulse Loader
Three-note loader with staggered radial pulse and optional progress text. Reduced-motion safe and tiny — 0.8 KB gzipped.
Learn essays that teach the technique
One original micro-case
Inspiration context
The settings row that got fewer support tickets
A settings screen had seventeen identical switches with no press state. Users could not tell whether a tap had registered, so they tapped twice — and support logs filled with 'it toggles back'. The fix was one press-state scale plus a 250ms state fade per row. Ticket volume on that screen dropped by half in a month.
Written in-house — no screenshots, no borrowed imagery. The pattern, not the pixels.
Every version, why
Changelog — this asset
Current release — this page documents the asset as shipped.
Token pass: colours and radii moved to the design-token sheet; a11y score raised to the current bar.
Initial release to the library — original markup, MIT licensed.
30-day sparkline
Copy history
Sample series from this asset's usage fingerprint · last 12 days shown.
Future queue
Community remixes
Queue opens after launch. Remixes arrive as alternate versions of this asset — same job, different voice — submitted by users and audited by the studio before they appear beside the original.
Star / favourite
Save this asset
Your collection is stored in this browser (localStorage) — no account needed.
Build a page around it
Pairs well with a prompt
This asset belongs inside a bigger build — here is the prompt that would generate a page containing it:
A11y-strict page — WCAG AA as a hard requirement
AA requirements · Contrast audit · Keyboard map
Exact tree
Bundled dependencies — disclosed
Full snippet ≈ 5.9 KB raw, ~1 KB gzip — before any framework you already load.
Download
Export as single file
Grab the asset as one file, ready to paste into your project.
Files are generated in your browser from the snippets on this page — nothing leaves the tab.
Scale, not clutter
Sizing system — three densities
Density is a token decision, not a per-page override — pick one density per product surface and hold it. Active: Default.
Runnable copy
Open in a sandbox
Opens this asset in a new tab with its stylesheet injected — a real, editable HTML document in your browser. No account, no upload.
If the tab is blocked, use the single-file export instead — same content, saved locally.
Written version first
Tutorial transcript
This asset does not have a video yet — so the written version stands in: the code line walk below narrates the snippet line by line, and the design rationale on this page explains the default decisions. When a video ships, this exact panel hosts its full transcript.
Annotated snippet
Code line walk
The lines that matter, with the reason they matter. Click one:
Trade-off, visible
Size vs. quality slider
Standard · CSS + hover states
~6 KBadds transitions, focus-visible and reduced-motion rules
The slider is honest: the slim tier has no JS, the pro tier pays for behaviour. Start slim, upgrade only the surfaces that need it.
HTML → React, priced
Stack-switch cost note
Taking the HTML/CSS version into a React component costs real work — and the cost is predictable:
Event wiring
Hover, focus and click become handlers — the CSS stays, the state appears.
Props for the knobs
Every inline value that should vary becomes a prop with a default — the defaults must match the CSS exactly.
The estimate
A clean port of this asset is 20–45 minutes for a React developer — the 259-character stylesheet is the easy half.
Find it the way you say it
Tag synonyms
Search on this site resolves synonyms globally — "dropdown", "select" and "picker" land on the same asset, so you never need to know the catalog's internal vocabulary.
When to pick which
Sibling comparison
| Compare | Table of Contents Spine | Prism Switch | Halo Button |
|---|---|---|---|
| Kind | Elements | Elements | Elements |
| Bundle | 2.4 KB gzip | 1.2 KB gzip | 2.1 KB gzip |
| a11y | 96/100 | 98/100 | 96/100 |
| Dependencies | none | none | none |
| Copies / mo | 420 | 4,820 | 3,910 |
| Interactions | click, scroll | click, hover | hover, click, drag |
The honest verdict — Prism Switch is lighter, but Table of Contents Spine carries richer behaviour — pick by the interaction your screen actually needs, then swap tokens, not pages.
Similar to this — scored on shared tags, not hand-picked
Related assets
Prism Switch
1.2 KB · a11y 98 · Q 96
Halo Button
2.1 KB · a11y 96 · Q 94
Pulse Loader
0.8 KB · a11y 99 · Q 93
Read next — two guides and a prompt
picked by tag overlap, not by handGuide
A form is a conversation
8 min · Beginner · updated 2026-09-12
Guide
Before you copy that component: the a11y checklist
8 min · Beginner · updated 2026-09-06
Prompt
3D portfolio with neon orbit showcase
Portfolio · 87% fidelity
How these three were chosen: essays scored on shared tags (0 and 0 matches), then prompt scored on block/vibe overlap (0 matches). The rule lives in src/lib/spine.ts, so the same asset always links to the same three pages and a second reader can reproduce the choice.
The counter is a note to yourself — it adds one each time you tap, in motif:remix-count:v1, and goes nowhere. A real, public remix count would need a server and accounts; this site has neither, and it would rather say that than print a number it cannot own.