Skip to Content
DocsGetting Started

Getting Started

Installation

Run the following command 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
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:

  • 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/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:

  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
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.

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

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
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:

TocIntersectionObserverOptions.tsx
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:

TocDeps.tsx
"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:

TocOnActiveIdChange.tsx
"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:

TocHeadingText.tsx
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:

TocHeadingVisible.tsx
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:

TocScrollToOptions.tsx
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).

TocExpandAll.tsx
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.

TocExpandDepth.tsx
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.

TocTopBarExample.tsx
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> ); };
TocTopBarExample

k.1. Heading

k.2. Heading

k.2.1. Heading

k.2.2. Heading

k.2.3. Heading

k.2.3.1. Heading

k.2.3.2. Heading

k.2.3.3. Heading

k.2.3.3.1. Heading
k.2.3.3.1.1. Heading

k.2.4. Heading

k.3. Heading

Last updated on