Match item IDs to headings in the same scrolling area. Pass container for a scrollable article, or omit it to track the page. The rail keeps the active item in view and respects reduced motion.
Installation
$pnpm dlx shadcn@latest add https://your-docs-domain.com/r/reading-rail.json
Usage
"use client"
import { useState } from "react"
import { ReadingRail } from "@/components/ui/reading-rail"
export default function Demo() {
const [container, setContainer] = useState<HTMLDivElement | null>(
null,
)
const items = [
{ id: "demo-material", label: "The material" },
{ id: "demo-edge", label: "The edge", depth: 1 },
{ id: "demo-motion", label: "The movement" },
{ id: "demo-response", label: "The response", depth: 1 },
]
return (
<div className="ag-rail-example">
<ReadingRail
title="Contents"
container={container}
items={items}
/>
<div ref={setContainer} className="ag-rail-article">
{items.map((item) => (
<section key={item.id} id={item.id}>
<h2>{item.label}</h2>
<p>
A shared material, a considered edge, and motion that
follows your touch.
</p>
<p>Select a heading or scroll to follow the path.</p>
</section>
))}
</div>
</div>
)
}
API
| Property | Type | Description |
|---|
items | ReadingRailItem[] | Ordered headings. Each id must match a heading in the content. |
container? | HTMLElement | null | Scrollable element to track. Omit it to follow page scrolling. |
offset? | number | Distance from the top where a heading becomes active, in pixels. |
title? | string | Visible title and accessible navigation label. |
activeId? | string | Selected item when active state is controlled. |
onActiveIdChange? | (id: string) => void | Receives selection changes in controlled mode. |
size? | "default" | "large" | Use large for a standalone contents view. |
Standard element attributes are supported where the props extend a React element type. Required properties appear without a question mark.
Dependencies
motion
Shared local files are included in the Code panel. See project setup for Tailwind, path aliases, and theme tokens.