INFINITYUI
HomeComponentsDocsTemplates
Star

Getting Started

  • Introduction
  • Installation
  • CLI
  • Audit

Navigation

  • Spotlight Navbar
  • Glass DockNEW
  • Animated Tab Bar
  • Circle Menu
  • Magnet Tabs
  • Animated Sidebar
  • Apple Spotlight
  • Page TOC RailNEW

Text

  • Flip Text
  • Glitch Text
  • Liquid Text
  • Flip Fade Text
  • Mask Cursor Effect

Cards

  • Glow Border Card
  • Testimonials Card
  • Interactive Book
  • Trading CardsNEW
  • Hover Image
  • Chain of ThoughtNEW
  • Masonry Grid
  • Image Pile
  • Staggered Grid

Inputs

  • AI InputNEW
  • OTP Input
  • Leave Rating

Buttons

  • Social Flip Button
  • Creepy Button

Loaders

  • Jelly Loader
  • Rolling Ball Scroll
  • Glowing Scroll

Backgrounds

  • Light Lines
  • Perspective Grid
  • Liquid Ocean
  • Eagle Vision
  • Flow Scroll
  • Horizontal Scroll

Overlays

  • PersonaNEW
  • Infinite Moving Cards
  • Masked Avatars
  • Stacked Logos
  • Icon Wheel
  • Pixelated CarouselNEW
  • Pixelated Image Trail
  • Flip Scroll
  • Interactive Folder
  • Animated Folder IconNEW
  • Stack Scroll
  • Rubik Cube
NavigationNEW

Page TOC Rail

#toc#sidebar#docs

On this page

Installation
Usage with AI SDK
Features
Examples
Grid Variant
Inline Variant
List Variant
Props
<Attachments />
<Attachment />
<AttachmentPreview />
<AttachmentInfo />
<AttachmentRemove />
<AttachmentHoverCard />
<AttachmentEmpty />
Utility Functions
getMediaCategory(data)
getAttachmentLabel(data)
Edit this page on GitHub
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-rail
Package Dependencies

Install the npm packages used by this component source.

npm install clsx lucide-react tailwind-merge

Component 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;