Skip to Content
DocsGetting Started

Getting Started

Installation

Run the following commands in the React project directory:

npm:

npm install @hyunjinno/react-toc

yarn:

yarn add @hyunjinno/react-toc

pnpm:

pnpm add @hyunjinno/react-toc

Quick Start

Step 1.

Import the CSS file at the entry point of the application

import "@hyunjinno/react-toc/style.css";

Step 2.

Wrap your content with TocProvider. Use the provided heading components (H2 ~ H6) inside a TocProvider. Then place the Toc component wherever you want the table of contents to appear inside a TocProvider.

TocBasic.tsx
"use client"; import { H2, H3, H4, H5, H6, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocBasic = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow"> <div className="flex flex-col gap-4"> <H2>a.1. Heading</H2> <H2>a.2. Heading</H2> <H3>a.2.1. Heading</H3> <H3>a.2.2. Heading</H3> <H3>a.2.3. Heading</H3> <H4>a.2.3.1. Heading</H4> <H4>a.2.3.2. Heading</H4> <H4>a.2.3.3. Heading</H4> <H5>a.2.3.3.1. Heading</H5> <H6>a.2.3.3.1.1. Heading</H6> <H3>a.2.4. Heading</H3> <H2>a.3. Heading</H2> </div> <Toc className="w-44" /> </TocProvider> ); };

a.1. Heading

a.2. Heading

a.2.1. Heading

a.2.2. Heading

a.2.3. Heading

a.2.3.1. Heading

a.2.3.2. Heading

a.2.3.3. Heading

a.2.3.3.1. Heading
a.2.3.3.1.1. Heading

a.2.4. Heading

a.3. Heading

Contents

Provided Heading Components

The library exports the following components for each heading level:

  • H2
  • H3
  • H4
  • H5
  • H6

All of them accept the same props as their native HTML counterparts (ex. H2 accepts all props that a normal <h2> would). They automatically:

  • Add the class toc-heading.
  • Defer id generation to the TocProvider.

How id is generated

Unlike typical approaches, heading ids are not generated during render.

Instead, TocProvider assigns unique, URL-friendly ids on the client side by scanning all .toc-heading elements after mount.

This approach ensures:

  • No hydration mismatch in SSR environments (ex. Next.js).
  • Stable and predictable id generation.
  • Automatic handling of duplicate headings.
import { H2 } from "@hyunjinno/react-toc"; <H2 style={{ color: "blue" }}>My Section</H2>; // After hydration: <h2 class="toc-heading" id="my-section" style="color: blue;">My Section</h2>

If multiple headings have the same text, unique suffixes are automatically appended:

- `my-section` - `my-section-1` - `my-section-2`

Styling

The library provides minimal default styling, allowing you to fully customize its appearance.

You can style the TOC in two ways:

  1. Passing class names via props
  2. Overriding the default CSS classes

Customizing with Props

Toc component accepts the following props for styling:

PropTypeDefaultDescription
className?string-CSS class for the outer <section> element.
headingClassName?string-CSS class for the optional heading (ex. “Contents”).
listClassName?string-CSS class for all <ul> elements in the TOC.
linkClassName?string-CSS class for all <a> links.
activeClassName?string-Additional CSS class applied to the active link.
TocCSS.tsx
"use client"; import { H2, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocCSS = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow"> <div className="flex flex-col gap-4"> <H2>b.1. Heading</H2> <H2>b.2. Heading</H2> <H2>b.3. Heading</H2> <H2>b.4. Heading</H2> <H2>b.5. Heading</H2> </div> <Toc className="w-60 bg-blue-50" headingClassName="text-teal-500 text-xl pb-5" listClassName="-mt-2" linkClassName="text-blue-300" activeClassName="font-semibold text-lg" /> </TocProvider> ); };

b.1. Heading

b.2. Heading

b.3. Heading

b.4. Heading

b.5. Heading

Contents

Overriding Default Styles

The library includes predefined CSS class names that you can override.

ClassDescription
.react-toc-wrapperRoot container of the TOC
.react-toc-wrapper-headingTOC title (ex. “Contents”)
.react-toc-list<ul> list container
.react-toc-linkEach TOC link
.react-toc-activeActive TOC link
.react-toc-h2 ~ .react-toc-h6Indentation per heading level
style.css
.react-toc-wrapper { display: flex; flex-direction: column; gap: 1rem; border-left: 1px solid #dddddd; height: fit-content; padding-bottom: 1rem; } .react-toc-wrapper-heading { padding-left: 1rem; font-weight: 500; } .react-toc-list { display: flex; flex-direction: column; gap: 0.625rem; } .react-toc-link { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; &:hover { color: #0056b2; } } .react-toc-active { border-left: 1px solid #0056b2; color: #0056b2; margin-left: -1px; } .react-toc-h2 { padding-left: 1rem; } .react-toc-h3 { padding-left: 1.75rem; } .react-toc-h4 { padding-left: 2.5rem; } .react-toc-h5 { padding-left: 3.25rem; } .react-toc-h6 { padding-left: 4rem; }

You can override them by writing your own styles targeting these class names.

Make sure your CSS is loaded after the library’s styles.

my-style.css
.react-toc-link { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; &:hover { color: #00ff00; } } .react-toc-active { border-left: 1px solid #00ff00; color: #00ff00; margin-left: -1px; }
import "@hyunjinno/react-toc/style.css"; import "my-style.css";

TocProvider

maxDepth

Limit the TOC to only H2 and H3 headings:

TocMaxDepth.tsx
"use client"; import { H2, H3, H4, H5, H6, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocMaxDepth = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow" maxDepth={2} // H2 ~ H3 only > <div className="flex flex-col gap-4"> <H2>c.1. Heading</H2> <H2>c.2. Heading</H2> <H3>c.2.1. Heading</H3> <H4>c.2.1.1. Heading</H4> <H5>c.2.1.1.1. Heading</H5> <H6>c.2.1.1.1.1. Heading</H6> <H3>c.2.2. Heading</H3> <H2>c.3. Heading</H2> </div> <Toc className="w-44" /> </TocProvider> ); };

c.1. Heading

c.2. Heading

c.2.1. Heading

c.2.1.1. Heading

c.2.1.1.1. Heading
c.2.1.1.1.1. Heading

c.2.2. Heading

c.3. Heading

Contents

observerOptions

TocProvider tracks the active heading via an Intersection Observer. You can customize intersection observer options:

TocIntersectionObserverOptions.tsx
"use client"; import { H2, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocIntersectionObserverOptions = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow" observerOptions={{ rootMargin: "-50% 0px -50% 0px" }} > <div className="flex flex-col gap-4"> <H2>d.1. Heading</H2> <H2>d.2. Heading</H2> <H2>d.3. Heading</H2> <H2>d.4. Heading</H2> <H2>d.5. Heading</H2> </div> <Toc className="w-44" /> </TocProvider> ); };

d.1. Heading

d.2. Heading

d.3. Heading

d.4. Heading

d.5. Heading

Contents

deps

If your content changes after mount, you need to trigger a rebuild of the TOC. Pass a dependency array via the deps prop:

TocDeps.tsx
"use client"; import { H2, Toc, TocProvider } from "@hyunjinno/react-toc"; import { useState } from "react"; export const TocDeps = () => { const [headingList, setHeadingList] = useState<string[]>([]); const [count, setCount] = useState(0); const handleClick = () => { setHeadingList([...headingList, crypto.randomUUID()]); setCount(count + 1); }; return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow" deps={[count]} > <div className="flex flex-col gap-4"> {headingList.map((value) => ( <H2 key={value}>{value}</H2> ))} <button className="h-10 w-10 cursor-pointer rounded-full border border-gray-200 bg-white p-1 shadow hover:scale-105" onClick={handleClick} > + </button> </div> <Toc className="w-44" /> </TocProvider> ); };

Contents

onActiveIdChange

React to active heading changes, e.g., for analytics:

TocOnActiveIdChange.tsx
"use client"; import { H2, H3, H4, H5, H6, Toc, TocProvider } from "@hyunjinno/react-toc"; import { useState } from "react"; export const TocOnActiveIdChange = () => { const [activeId, setActiveId] = useState(""); const [activeHeadingContent, setActiveHeadingContent] = useState(""); const handleActiveIdChange = (id: string, textContent: string) => { setActiveId(id); setActiveHeadingContent(textContent); }; return ( <TocProvider className="mt-5 flex flex-col rounded-lg border border-gray-100 p-4 shadow" observerOptions={{ rootMargin: "-50% 0px -50% 0px" }} onActiveIdChange={handleActiveIdChange} > <p> Active ID: <span className="text-blue-500">{activeId}</span> </p> <p> Active Heading Content:{" "} <span className="text-blue-500">{activeHeadingContent}</span> </p> <div className="mt-5 flex flex-row justify-between"> <div className="flex flex-col gap-4"> <H2>e.1. Heading</H2> <H2>e.2. Heading</H2> <H3>e.2.1. Heading</H3> <H3>e.2.2. Heading</H3> <H3>e.2.3. Heading</H3> <H4>e.2.3.1. Heading</H4> <H4>e.2.3.2. Heading</H4> <H4>e.2.3.3. Heading</H4> <H5>e.2.3.3.1. Heading</H5> <H6>e.2.3.3.1.1. Heading</H6> <H3>e.2.4. Heading</H3> <H2>e.3. Heading</H2> </div> <Toc className="w-44" /> </div> </TocProvider> ); };

Active ID:

Active Heading Content:

e.1. Heading

e.2. Heading

e.2.1. Heading

e.2.2. Heading

e.2.3. Heading

e.2.3.1. Heading

e.2.3.2. Heading

e.2.3.3. Heading

e.2.3.3.1. Heading
e.2.3.3.1.1. Heading

e.2.4. Heading

e.3. Heading

Contents

Toc

headingText

You can change TOC title:

TocHeadingText.tsx
"use client"; import { H2, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocHeadingText = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow"> <div className="flex flex-col gap-4"> <H2>f.1. Heading</H2> <H2>f.2. Heading</H2> <H2>f.3. Heading</H2> <H2>f.4. Heading</H2> <H2>f.5. Heading</H2> </div> <Toc className="w-44" headingClassName="text-blue-500" headingText="On This Page" /> </TocProvider> ); };

f.1. Heading

f.2. Heading

f.3. Heading

f.4. Heading

f.5. Heading

On This Page

headingVisible

The TOC title can be hidden:

TocHeadingVisible.tsx
"use client"; import { H2, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocHeadingVisible = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow"> <div className="flex flex-col gap-4"> <H2>g.1. Heading</H2> <H2>g.2. Heading</H2> <H2>g.3. Heading</H2> <H2>g.4. Heading</H2> <H2>g.5. Heading</H2> </div> <Toc className="w-44" headingVisible={false} /> </TocProvider> ); };

g.1. Heading

g.2. Heading

g.3. Heading

g.4. Heading

g.5. Heading

Custom ScrollToOptions

You can customize ScrollToOptions passed to window.scrollTo when a link is clicked:

TocScrollToOptions.tsx
"use client"; import { H2, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocScrollToOptions = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow"> <div className="flex flex-col gap-4"> <H2>h.1. Heading</H2> <H2>h.2. Heading</H2> <H2>h.3. Heading</H2> <H2>h.4. Heading</H2> <H2>h.5. Heading</H2> </div> <Toc className="w-44" offsetTop={100} offsetLeft={0} scrollBehavior="instant" /> </TocProvider> ); };

h.1. Heading

h.2. Heading

h.3. Heading

h.4. Heading

h.5. Heading

Contents

expandAll

The expandAll prop determines whether all nested levels are always visible (true) or only the branch containing the active heading is expanded (false).

TocExpandAll.tsx
"use client"; import { H2, H3, H4, H5, H6, Toc, TocProvider } from "@hyunjinno/react-toc"; export const TocExpandAll = () => { return ( <TocProvider className="mt-5 flex flex-row justify-between rounded-lg border border-gray-100 p-4 shadow"> <div className="flex flex-col gap-4"> <H2>i.1. Heading</H2> <H2>i.2. Heading</H2> <H3>i.2.1. Heading</H3> <H3>i.2.2. Heading</H3> <H3>i.2.3. Heading</H3> <H4>i.2.3.1. Heading</H4> <H4>i.2.3.2. Heading</H4> <H4>i.2.3.3. Heading</H4> <H5>i.2.3.3.1. Heading</H5> <H6>i.2.3.3.1.1. Heading</H6> <H3>i.2.4. Heading</H3> <H2>i.3. Heading</H2> </div> <Toc className="w-44" expandAll={true} /> </TocProvider> ); };

i.1. Heading

i.2. Heading

i.2.1. Heading

i.2.2. Heading

i.2.3. Heading

i.2.3.1. Heading

i.2.3.2. Heading

i.2.3.3. Heading

i.2.3.3.1. Heading
i.2.3.3.1.1. Heading

i.2.4. Heading

i.3. Heading

Contents

Last updated on