Section Rail
A responsive section navigation rail with scroll tracking, smooth anchor jumps, link mode, and optional rich hover previews.
A calmer table of contents
Long-form pages should stay readable without making navigation feel heavy. The rail turns the document outline into a quiet waveform at the edge of the page.
- hover a mark to reveal its label
- select one to jump to that section
- scroll to watch the active mark travel
Structure
Each rail item points to an element with the same id. The content can come from Markdown, MDX, a CMS, or ordinary React components.
Keep section IDs unique when more than one document appears on a page.
Scroll behavior
An intersection observer follows the section nearest the reading band. Selecting a mark scrolls smoothly, while reduced motion preferences switch the jump to instant scrolling.
The contained variant can track an overflow panel like this preview instead of the browser viewport.
Ready to publish
Use the default viewport variant for articles and documentation pages. Use variant="contained" with a scrollRootRef for dialogs, side panels, and component previews.
That is the whole pattern: stable IDs, useful labels, and content worth navigating.
Installation
Section Rail is published as a shadcn registry item. The CLI drops the file into your project, installs its dependencies, and adds any primitives it relies on.
$npx shadcn@latest add @akoder/section-railFirst time? Register the @akoder namespace once in components.json and every component installs by name.
"registries": {
"@akoder": "https://akoder.xyz/r/{name}.json"
}Usage
Give each rail item the id of its target section. Without an href, the rail follows the reading position and scrolls to sections when selected.
"use client";
import { SectionRail, type RailItem } from "@/components/section-rail";
const items: RailItem[] = [
{ id: "overview", label: "overview" },
{ id: "installation", label: "installation" },
{ id: "api", label: "api" },
];
export function Article() {
return (
<>
<SectionRail items={items} />
<article>
<section id="overview">...</section>
<section id="installation">...</section>
<section id="api">...</section>
</article>
</>
);
}Contained scroll areas
Use the contained variant for a dialog, drawer, or overflow panel. The rail stays attached to the surrounding relative container while observing and scrolling the referenced element.
"use client";
import { useRef } from "react";
import ReactMarkdown from "react-markdown";
import { SectionRail } from "@/components/section-rail";
const sections = [
{ id: "intro", label: "intro", body: "## Introduction\n\nStart here." },
{ id: "details", label: "details", body: "## Details\n\nKeep reading." },
];
export function MarkdownGuide() {
const scrollRootRef = useRef<HTMLDivElement>(null);
return (
<div className="relative">
<SectionRail
items={sections}
variant="contained"
scrollRootRef={scrollRootRef}
/>
<div ref={scrollRootRef} className="h-96 overflow-y-auto pl-20">
{sections.map((section) => (
<section key={section.id} id={section.id} className="min-h-96">
<ReactMarkdown>{section.body}</ReactMarkdown>
</section>
))}
</div>
</div>
);
}Link rail
Items with an href navigate instead of scrolling. Pass activeId to pin the current page.
<SectionRail
activeId="guides"
items={[
{ id: "home", label: "home", href: "/" },
{ id: "guides", label: "guides", href: "/guides" },
]}
/>Props
| Prop | Type | Default | Notes |
|---|---|---|---|
items | RailItem[] | required | Ordered marks. Each item needs id and label; add href for navigation or card for a rich tooltip. |
activeId | string | — | Pins the active mark. Most useful for link rails. |
variant | "viewport" | "contained" | "viewport" | Viewport rails are fixed and appear at the lg breakpoint; contained rails are absolutely positioned and always visible. |
scrollRootRef | RefObject<HTMLElement | null> | — | Overflow element that owns the target sections in contained mode. |
className | string | — | Additional classes for positioning or styling the rail. |
RailItem
| Field | Type | Notes |
|---|---|---|
id | string | Unique item key and the target element ID for scroll items. |
label | string | Accessible name and default tooltip copy. |
href | string | Turns the mark into a Next.js link. |
card | { title; description?; icon? } | Replaces the plain tooltip with a rich preview card. |
Notes & features
- Scroll tracking. An intersection observer selects the target nearest the center reading band.
- Reliable final section. Reaching the bottom of the viewport or contained scroller activates the last item even when it is short.
- Reduced motion. Section jumps honor
prefers-reduced-motion. - Accessible controls. Scroll marks are buttons, links stay links, and
aria-currentidentifies the active destination. - Stable targets. IDs must be unique in the document. When
scrollRootRefis provided, targets outside that element are ignored. - Tooltip setup. Mount shadcn's
TooltipProvideronce near the root of your application.
Manual installation
Copy components/section-rail.tsx into your project. Install the shadcn tooltip primitive and ensure the standard @/lib/utils cn helper is available. The link mode uses Next.js Link; replace it with an anchor or your router's link component outside Next.js.