Getting Started
Installation
Run the following command 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.
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2, H3, H4, H5, H6 } from "@hyunjinno/react-toc/heading";
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/heading";
<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. |
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2 } from "@hyunjinno/react-toc/heading";
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.
.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;
}
.react-toc-top-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 1rem;
height: 3rem;
background-color: white;
padding-inline: 1rem;
border-bottom: 1px solid #dddddd;
}
.react-toc-top-bar-title,
.react-toc-modal-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.react-toc-list-icon,
.react-toc-close-icon {
cursor: pointer;
&:hover {
fill: #0056b2;
}
}
.react-toc-modal-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.react-toc-modal {
display: flex;
flex-direction: column;
width: 24rem;
max-width: calc(100vw - 1rem);
max-height: calc(100vh - 1rem);
background-color: white;
border-radius: 0.75rem;
}
.react-toc-modal-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
padding-inline: 1rem;
padding-block: 0.5rem;
border-bottom: 1px solid #dddddd;
}
.react-toc-modal-content {
padding-block: 1rem;
overflow: auto;
}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:
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2, H3, H4, H5, H6 } from "@hyunjinno/react-toc/heading";
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:
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2 } from "@hyunjinno/react-toc/heading";
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 { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2 } from "@hyunjinno/react-toc/heading";
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 { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2, H3, H4, H5, H6 } from "@hyunjinno/react-toc/heading";
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:
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2 } from "@hyunjinno/react-toc/heading";
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:
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2 } from "@hyunjinno/react-toc/heading";
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:
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2 } from "@hyunjinno/react-toc/heading";
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).
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2, H3, H4, H5, H6 } from "@hyunjinno/react-toc/heading";
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
expandDepth
The expandDepth prop specifies the maximum depth of headings that should be expanded by default in the TOC.
All headings at or below this depth will be always visible, regardless of the current active heading.
import { Toc, TocProvider } from "@hyunjinno/react-toc";
import { H2, H3, H4, H5, H6 } from "@hyunjinno/react-toc/heading";
export const TocExpandDepth = () => {
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>j.1. Heading</H2>
<H2>j.2. Heading</H2>
<H3>j.2.1. Heading</H3>
<H3>j.2.2. Heading</H3>
<H3>j.2.3. Heading</H3>
<H4>j.2.3.1. Heading</H4>
<H4>j.2.3.2. Heading</H4>
<H4>j.2.3.3. Heading</H4>
<H5>j.2.3.3.1. Heading</H5>
<H6>j.2.3.3.1.1. Heading</H6>
<H3>j.2.4. Heading</H3>
<H2>j.3. Heading</H2>
</div>
<Toc className="w-44" expandDepth={2} />
</TocProvider>
);
};j.1. Heading
j.2. Heading
j.2.1. Heading
j.2.2. Heading
j.2.3. Heading
j.2.3.1. Heading
j.2.3.2. Heading
j.2.3.3. Heading
j.2.3.3.1. Heading
j.2.3.3.1.1. Heading
j.2.4. Heading
j.3. Heading
Contents
TocTopBar
The <TocTopBar> component is a container for the TOC modal, providing a heading and an icon to open it.
import { TocProvider, TocTopBar } from "@hyunjinno/react-toc";
import { H2, H3, H4, H5, H6 } from "@hyunjinno/react-toc/heading";
export const TocTopBarExample = () => {
return (
<TocProvider className="mt-5 flex flex-col gap-4 truncate rounded-lg border border-gray-100 p-4 shadow">
<TocTopBar title="TocTopBarExample" expandAll={true} />
<div className="flex flex-col gap-4">
<H2>k.1. Heading</H2>
<H2>k.2. Heading</H2>
<H3>k.2.1. Heading</H3>
<H3>k.2.2. Heading</H3>
<H3>k.2.3. Heading</H3>
<H4>k.2.3.1. Heading</H4>
<H4>k.2.3.2. Heading</H4>
<H4>k.2.3.3. Heading</H4>
<H5>k.2.3.3.1. Heading</H5>
<H6>k.2.3.3.1.1. Heading</H6>
<H3>k.2.4. Heading</H3>
<H2>k.3. Heading</H2>
</div>
</TocProvider>
);
};