Getting Started
Installation
Run the following commands in the React project directory:
npm:
npm install @hyunjinno/react-tocyarn:
yarn add @hyunjinno/react-tocpnpm:
pnpm add @hyunjinno/react-tocQuick 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.
"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:
H2H3H4H5H6
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
idgeneration to theTocProvider.
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
idgeneration. - 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:
- Passing class names via props
- Overriding the default CSS classes
Customizing with Props
Toc component accepts the following props for styling:
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
"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.
| Class | Description |
|---|---|
.react-toc-wrapper | Root container of the TOC |
.react-toc-wrapper-heading | TOC title (ex. “Contents”) |
.react-toc-list | <ul> list container |
.react-toc-link | Each TOC link |
.react-toc-active | Active TOC link |
.react-toc-h2 ~ .react-toc-h6 | Indentation per heading level |
.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.
.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:
"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:
"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:
"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:
"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:
"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:
"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:
"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).
"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>
);
};