Hasan Harman
Lab

Table of Contents

The Notion-style floating ToC from my writeups — collapsed tick marks that expand into the full outline on hover.

import { Link } from "next-view-transitions";
import React from "react";

import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

interface TocItem {
  title: string;
  url: string;
  items?: TocItem[];
}

interface TableOfContentsProps {
  tocList: TocItem[];
}

export default function TableOfContents({ tocList }: TableOfContentsProps) {
  return (
    <>
      <div className="hidden md:block">
        <HoverCard>
          <HoverCardTrigger asChild>
            <div className="absolute right-1 space-y-2">
              {tocList.map((item, index) => (
                <div
                  className="flex flex-col items-end space-y-2"
                  key={item.title}
                >
                  <div className="w-7 flex flex-col items-end h-0.5 bg-muted-foreground/50 hover:bg-foreground rounded-xl transition-colors" />
                  {item.items && item.items.length > 0 && (
                    <div className="flex flex-col space-y-2">
                      {item.items.map((subItem, subIndex) => (
                        <div
                          key={subItem.title}
                          className="w-5 flex flex-col items-end h-0.5 bg-muted-foreground/50 hover:bg-foreground rounded-xl transition-colors"
                        />
                      ))}
                    </div>
                  )}
                </div>
              ))}
            </div>
          </HoverCardTrigger>
          <HoverCardContent className="w-fit p-3 space-y-2 ">
            {tocList.map((item, index) => (
              <div className="space-y-2" key={item.title}>
                <Link
                  href={item.url}
                  className="text-sm no-underline hover:underline"
                >
                  {item.title}
                </Link>
                {item.items && item.items.length > 0 && (
                  <div className="flex flex-col space-y-2">
                    {item.items.map((subItem, subIndex) => (
                      <Link
                        href={subItem.url}
                        key={subItem.title}
                        className="text-xs ml-3 no-underline hover:underline"
                      >
                        {subItem.title}
                      </Link>
                    ))}
                  </div>
                )}
              </div>
            ))}
          </HoverCardContent>
        </HoverCard>
      </div>
      <Card className="md:hidden">
        <CardHeader className="pb-3">
          <CardTitle className="m-0">Table of Contents</CardTitle>
        </CardHeader>
        <CardContent>
          {tocList.map((item, index) => (
            <div className="space-y-2" key={item.title}>
              <Link
                href={item.url}
                className="text-sm no-underline hover:underline"
              >
                {item.title}
              </Link>
              {item.items && item.items.length > 0 && (
                <div className="flex flex-col space-y-2">
                  {item.items.map((subItem, subIndex) => (
                    <Link
                      href={subItem.url}
                      key={subItem.title}
                      className="text-xs ml-3 no-underline hover:underline"
                    >
                      {subItem.title}
                    </Link>
                  ))}
                </div>
              )}
            </div>
          ))}
        </CardContent>
      </Card>
    </>
  );
}

Installation

$ pnpm dlx shadcn@latest add https://hasanharman.dev/r/table-of-contents.json

Usage

import TableOfContents from "@/components/table-of-contents";

const toc = [
  { title: "Getting started", url: "#getting-started" },
  { title: "Usage", url: "#usage" },
];

export default function Example() {
  return <TableOfContents tocList={toc} />;
}