appica-dev

Table of Contents

Navigation

About

A scroll-spy "on this page" navigation sidebar that highlights the heading currently in view as the user scrolls the article.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Table of Contents to your project and fits it to your theme.

Code

How Table of Contents is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import { type MouseEvent } from "react";
import { Toc, TocList, TocItem, TocLink } from "@/components/ui/toc";

const HEADINGS = [
  {
    id: "toc-nested-install",
    title: "Installation",
    depth: 2,
    body: "Add the package to your project before importing anything. It has a single peer dependency on React and works with any bundler that understands ES modules, so there is no special configuration to add and nothing to register globally. Pick the package manager your team already uses - the install command is the only thing that differs between them, and the resulting dependency is identical. In a monorepo, add it to the package that actually renders UI rather than the workspace root, so each app pins the version it depends on and upgrades stay scoped. Commit your lockfile afterwards so every environment, including CI, resolves exactly the same build of the library. Once it is installed you can import any component from its own subpath and start composing immediately, with no build step and no global stylesheet to wire up first.",
  },
  {
    id: "toc-nested-pnpm",
    title: "pnpm",
    depth: 3,
    body: "Run pnpm add to install it into the current workspace. In a monorepo, scope it to the package that renders UI rather than the root so versions stay explicit per app.",
  },
  {
    id: "toc-nested-npm",
    title: "npm",
    depth: 3,
    body: "npm install works identically and writes the dependency to your package.json. Commit the lockfile so every environment resolves the same version of the library.",
  },
  {
    id: "toc-nested-usage",
    title: "Usage",
    depth: 2,
    body: "Import the parts you need and compose them. Components are designed to be assembled rather than configured through long prop lists, so most screens are just JSX. The pieces share a styling context, so a variant set once flows down.",
  },
  {
    id: "toc-nested-import",
    title: "Importing",
    depth: 3,
    body: "Each component lives at its own subpath, so import from there rather than a barrel. This keeps your bundle lean and makes it obvious which parts a file actually depends on.",
  },
  {
    id: "toc-nested-compose",
    title: "Composing",
    depth: 3,
    body: "Nest the sub-components to build the shape you want, and reach for the render prop when you need to project a part onto your own element. Composition is the primary extension point across the whole library.",
  },
  {
    id: "toc-nested-faq",
    title: "FAQ",
    depth: 2,
    body: "Answers to the questions that come up most often, from server-component support to bundle size and theming. If something is missing here, the per-component pages go deeper.",
  },
];

// Demo only: scrolls within this panel instead of the page so the example stays put.
// A page-level TOC needs none of this - let the anchor jump the page natively.
function scrollWithinPanel(event: MouseEvent<HTMLElement>) {
  const link = (event.target as HTMLElement).closest('a[href^="#"]');
  if (!link) return;
  const id = decodeURIComponent(link.getAttribute("href")!.slice(1));
  const heading = document.getElementById(id);
  const panel = heading?.closest("[data-scroll-panel]");
  if (!heading || !panel) return;
  event.preventDefault();
  const gap = parseFloat(getComputedStyle(heading).scrollMarginTop) || 0;
  const top =
    heading.getBoundingClientRect().top -
    panel.getBoundingClientRect().top +
    panel.scrollTop -
    gap;
  panel.scrollTo({ top, behavior: "smooth" });
}

export default function TocNested() {
  return (
    <div className="flex w-full max-w-xl gap-4">
      <div
        data-scroll-panel
        className="border-border h-64 flex-1 scrollbar-none overflow-auto rounded-lg border p-4"
      >
        {HEADINGS.map((heading) => (
          <section key={heading.id} className="mb-6 last:mb-0">
            {heading.depth === 2 ? (
              <h3
                id={heading.id}
                className="text-foreground-intense mb-2 scroll-mt-6 font-semibold"
              >
                {heading.title}
              </h3>
            ) : (
              <h4
                id={heading.id}
                className="text-foreground-intense mb-2 scroll-mt-6 text-sm font-medium"
              >
                {heading.title}
              </h4>
            )}
            <p className="text-sm leading-relaxed">{heading.body}</p>
          </section>
        ))}
      </div>

      <Toc className="w-38 shrink-0" onClick={scrollWithinPanel}>
        <TocList>
          {HEADINGS.map((heading) => (
            <TocItem key={heading.id}>
              <TocLink href={`#${heading.id}`} depth={heading.depth}>
                {heading.title}
              </TocLink>
            </TocItem>
          ))}
        </TocList>
      </Toc>
    </div>
  );
}

Dependencies

More by appica-dev

View all
  • Border Beam

    Border Beam

    43
  • Text Animate

    Text Animate

    19
  • Carousel

    Carousel

    14
  • Sparkline

    Sparkline

    7
  • Color Area

    Color Area

    6
  • Avatar

    Avatar

    6
  • Background Pattern

    Background Pattern

    3
  • Autocomplete

    Autocomplete

    2

More in Navigation

View all
  • Sidebar

    Sidebar

    235
  • Navbar with Theme Toggle

    Navbar with Theme Toggle

    43
  • Sidebar

    Sidebar

    339
  • Tabs

    Tabs

    60
  • Numberless Pagination with Text

    Numberless Pagination with Text

    11
  • Oversized 404 Numeral

    Oversized 404 Numeral

    50
  • Footer

    Footer

    446
  • Pagination

    Pagination

    43