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.jsonUsage
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} />;
}