NavigationNEW
Page TOC Rail
#toc#sidebar#docs
approximateReference recreation
Built from captured docs rail markup/reference rather than from an upstream packaged source component. It is close visually, but not parity-verified against a canonical implementation.
Installation
Use the registry command to add the component source, and install any package dependencies if needed.
Infinity Registry
Install the component source into your project with the shadcn CLI.
npx shadcn@latest add https://infinityui-pearl.vercel.app/r/page-toc-railPackage Dependencies
Install the npm packages used by this component source.
npm install clsx lucide-react tailwind-mergeComponent Code
Copy and paste this code into your component file.
tsx
"use client";
import {
ArrowUpCircle,
Copy,
ExternalLink,
Github,
List,
MessageCircle,
ThumbsUp,
} from "lucide-react";
import { cn } from "@/lib/utils";
export type PageTocItem = {
id: string;
label: string;
href: string;
level?: 0 | 1;
active?: boolean;
code?: boolean;
};
export type PageTocAction = {
id: string;
label: string;
href?: string;
icon?: React.ReactNode;
onClick?: () => void;
};
export interface PageTocRailProps {
title?: string;
items: PageTocItem[];
actions?: PageTocAction[];
className?: string;
}
const defaultActions: PageTocAction[] = [
{
id: "edit",
label: "Edit this page on GitHub",
href: "#",
icon: <Github className="size-3.5" />,
},
{
id: "top",
label: "Scroll to top",
icon: <ArrowUpCircle className="size-3.5" />,
},
{
id: "feedback",
label: "Give feedback",
icon: <ThumbsUp className="size-3.5" />,
},
{
id: "copy",
label: "Copy page",
icon: <Copy className="size-3.5" />,
},
{
id: "ask-ai",
label: "Ask AI about this page",
icon: <MessageCircle className="size-3.5" />,
},
{
id: "chat",
label: "Open in chat",
icon: <ExternalLink className="size-3.5" />,
},
];
function TocActionButton({ action }: { action: PageTocAction }) {
const content = (
<>
{action.icon}
<span>{action.label}</span>
</>
);
if (action.href) {
return (
<a
href={action.href}
className="flex items-center gap-1.5 text-sm text-zinc-500 transition-colors hover:text-zinc-200"
>
{content}
</a>
);
}
return (
<button
type="button"
onClick={action.onClick}
className="flex items-center gap-1.5 text-sm text-zinc-500 transition-colors hover:text-zinc-200"
>
{content}
</button>
);
}
export function PageTocRail({
title = "On this page",
items,
actions = defaultActions,
className,
}: PageTocRailProps) {
const firstActiveIndex = items.findIndex((item) => item.active);
const lastActiveIndex =
firstActiveIndex === -1
? -1
: items.reduce(
(last, item, index) => (item.active ? index : last),
firstActiveIndex
);
const firstActiveItem = firstActiveIndex >= 0 ? items[firstActiveIndex] : null;
const activeRailOffset =
firstActiveItem?.level === 1 ? 10 : 0;
const activeTop = firstActiveIndex >= 0 ? 16 + firstActiveIndex * 38 : 0;
const activeHeight =
firstActiveIndex >= 0 ? (lastActiveIndex - firstActiveIndex + 1) * 38 - 8 : 0;
return (
<aside
className={cn(
"flex h-[688px] w-[300px] flex-col overflow-hidden rounded-[28px] border border-white/8 bg-[linear-gradient(180deg,rgba(255,255,255,0.07),rgba(255,255,255,0.02))] p-5 text-zinc-100 shadow-[0_24px_80px_rgba(0,0,0,0.45)] backdrop-blur-xl",
className
)}
>
<div className="mb-3 inline-flex items-center gap-1.5 text-sm text-zinc-400">
<List className="size-4" />
<h3>{title}</h3>
</div>
<div className="relative min-h-0 flex-1 overflow-auto py-3 [scrollbar-width:none]">
<div className="pointer-events-none absolute left-0 top-0 h-full w-4">
<div className="absolute left-[3px] top-3 bottom-3 w-px bg-white/10" />
{firstActiveIndex >= 0 && (
<div
className="absolute rounded-full bg-white shadow-[0_0_14px_rgba(255,255,255,0.35)] transition-all duration-300"
style={{
left: activeRailOffset,
top: activeTop,
height: activeHeight,
width: 2,
}}
/>
)}
</div>
<div className="flex flex-col">
{items.map((item) => (
<a
key={item.id}
href={item.href}
className={cn(
"group relative py-1.5 text-sm text-zinc-500 transition-colors hover:text-zinc-200",
item.active && "font-semibold text-zinc-100",
item.level === 1 ? "pl-6" : "pl-3.5"
)}
>
<div
className={cn(
"absolute inset-y-0 w-px bg-white/10",
item.level === 1 ? "left-[10px]" : "left-0"
)}
/>
{item.level === 1 && (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
className="absolute left-0 top-0 size-4"
>
<line
x1="0"
y1="0"
x2="10"
y2="12"
className="stroke-white/10"
strokeWidth="1"
/>
</svg>
)}
<span
className={cn(
"inline-flex items-center rounded-md px-1.5 py-0.5 transition-colors",
item.code && "border border-white/8 bg-white/[0.03]",
item.active && !item.code && "bg-transparent",
item.active && item.code && "border-white/12 bg-white/[0.06] text-zinc-100"
)}
>
{item.code ? <code>{item.label}</code> : item.label}
</span>
</a>
))}
</div>
</div>
<div className="mt-3 space-y-3">
<div className="h-px w-full bg-white/8" />
<div className="space-y-3">
{actions.map((action) => (
<TocActionButton key={action.id} action={action} />
))}
</div>
</div>
</aside>
);
}
export default PageTocRail;