Render a PDF table of contents with react-pdf outline
Table of contents
react-pdf — using the Outline component to render a PDF’s bookmark tree, handle item clicks for page navigation, and style the outline with CSS.
<Outline>must be a child of<Document>— it renders the PDF’s bookmark tree as nested<ul>/<li>/<a>elements.- Wire up
onItemClick(on either<Document>or<Outline>) to receive{ dest, pageIndex, pageNumber }and drive page navigation. - Not every PDF has an outline. Check
onLoadSuccessfornull, and hide the sidebar accordingly. - For full control over the markup, read raw outline data via
useOutlineContextand render the tree yourself — see custom rendering modes and context hooks in react-pdf for the pattern.
The Outline component renders the PDF’s built-in bookmark tree (table of contents). Clicking an item navigates to that section of the document.
Basic usage
Render Outline as a child of Document alongside your page view:
import { Document, Page, Outline } from "react-pdf";
function PDFWithOutline() { const [numPages, setNumPages] = useState(null); const [currentPage, setCurrentPage] = useState(1);
return ( <Document file={file} onLoadSuccess={({ numPages }) => setNumPages(numPages)} onItemClick={({ pageNumber }) => setCurrentPage(pageNumber)} > <div style={{ display: "flex" }}> <aside style={{ width: 250, overflowY: "auto" }}> <Outline /> </aside> <main> <Page pageNumber={currentPage} /> </main> </div> </Document> );}Outline props
| Prop | Type | Description |
|---|---|---|
className | string/string[] | CSS class(es) alongside .react-pdf__Outline. |
inputRef | ref | Ref forwarded to the root <div>. |
onItemClick | function | Called when an outline item is clicked. |
onLoadSuccess | function | Called when outline is loaded. |
onLoadError | function | Called on error loading outline. |
Handling item clicks
The onItemClick callback (on either Document or Outline) receives:
<Outline onItemClick={({ dest, pageIndex, pageNumber }) => { // `dest`: the PDF destination object (for advanced navigation). // `pageIndex`: 0-indexed page number. // `pageNumber`: 1-indexed page number. console.log(`Navigate to page ${pageNumber}`); setCurrentPage(pageNumber); }}/>onItemClick on <Document> catches clicks from any child <Outline>. This is useful for centralized navigation handling so each outline doesn’t need its own handler.
Outline load callbacks
onLoadSuccess and onLoadError report whether the outline loaded, and with what:
<Outline onLoadSuccess={(outline) => { if (outline) { console.log("Outline loaded with items"); } else { console.log("PDF has no outline"); } }} onLoadError={(error) => { console.error("Failed to load outline:", error); }}/>Styling the outline
The outline renders as a nested list. Target it with CSS:
.react-pdf__Outline { padding: 16px;}
/* Top-level list. */.react-pdf__Outline ul { list-style: none; padding: 0; margin: 0;}
/* Nested lists (subsections). */.react-pdf__Outline ul ul { padding-left: 16px;}
/* Outline items. */.react-pdf__Outline li { margin: 2px 0;}
/* Clickable links. */.react-pdf__Outline a { display: block; padding: 4px 8px; color: #333; text-decoration: none; border-radius: 4px; font-size: 14px;}
.react-pdf__Outline a:hover { background-color: #f0f0f0;}Conditional rendering
Not all PDFs have an outline. Handle this gracefully:
function OutlineSidebar() { const [hasOutline, setHasOutline] = useState(true);
if (!hasOutline) return null;
return ( <aside className="outline-sidebar"> <h3>Table of Contents</h3> <Outline onLoadSuccess={(outline) => { if (!outline) setHasOutline(false); }} /> </aside> );}Complete sidebar layout
This puts it all together: an outline sidebar that hides itself when there’s no outline, plus page navigation:
function PDFViewerWithSidebar() { const [numPages, setNumPages] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [showOutline, setShowOutline] = useState(true);
return ( <Document file={file} onLoadSuccess={({ numPages }) => setNumPages(numPages)} onItemClick={({ pageNumber }) => setCurrentPage(pageNumber)} > <div className="pdf-layout"> {showOutline && ( <aside className="sidebar"> <Outline onLoadSuccess={(outline) => { if (!outline) setShowOutline(false); }} /> </aside> )} <main className="pdf-content"> <Page pageNumber={currentPage} /> <div className="page-nav"> <button disabled={currentPage <= 1} onClick={() => setCurrentPage((p) => p - 1)} > Previous </button> <span>{currentPage} / {numPages}</span> <button disabled={currentPage >= numPages} onClick={() => setCurrentPage((p) => p + 1)} > Next </button> </div> </main> </div> </Document> );}Key points
Outlinemust be a child ofDocument.- Not all PDFs have an outline — check
onLoadSuccessfor null. onItemClickgives youpageNumber(1-indexed) andpageIndex(0-indexed).onItemClickonDocumentalso catches outline clicks (useful for centralized navigation).- Style with CSS targeting
.react-pdf__Outlineclasses. - The outline renders as nested
<ul>/<li>/<a>elements.
Rendering the outline yourself
If you need full control over the markup — to use your own component library, add icons per level, or virtualize a deeply nested tree — read the raw outline data and render it yourself. react-pdf exposes useOutlineContext for children of <Outline>, and onLoadSuccess(outline) hands you the same tree structure. See custom rendering modes and context hooks in react-pdf for the full pattern.
How Nutrient Web SDK handles this
All the custom outline components, click handlers, and conditional rendering above reduce to a single line with Nutrient Web SDK:
// Built-in bookmark sidebar via `sidebarMode` — one line.instance.setViewState((v) => v.set("sidebarMode", NutrientViewer.SidebarMode.BOOKMARKS),);There’s no <Outline> component, no onItemClick handler, no conditional rendering for missing outlines, and no CSS styling. Nutrient’s built-in sidebar displays the bookmark tree with nested navigation, automatic scroll-to-page, and keyboard accessibility — all styled and ready to use.
Learn more about Nutrient Web SDK | Migration guide | Contact Sales
FAQ
Not every PDF has one. Outlines are an authoring choice — Word, InDesign, and LaTeX (with hyperref) typically generate them; quick exports from browsers or scanners usually don’t. Check onLoadSuccess — if it receives null, the PDF has no bookmark tree and there’s nothing for <Outline> to render.
pageIndex and pageNumber in onItemClick?pageIndex is 0-indexed (matches the underlying PDF.js page array); pageNumber is 1-indexed (matches user-facing UI like “Page 5 of 20”). Use whichever fits the rest of your code, but be consistent — mixing them is a common off-by-one source.
onItemClick on <Document> or <Outline>?Either works. Put it on <Document> when you want one handler for all outline activity in the tree. Put it on <Outline> when you have multiple outlines (rare) or want to attach behavior specific to that instance — for example, expanding/collapsing UI state.
react-pdf remounts the <Outline> when the parent <Document>’s file prop changes, so the new outline loads automatically. If you tracked hasOutline in component state (as in the conditional rendering example), reset it in a useEffect keyed on file so a missing-outline state from a previous PDF doesn’t carry over.
Yes — the outline renders as nested <ul>, <li>, and <a> elements under .react-pdf__Outline. Override colors, spacing, and hover states with normal CSS. For richer control (custom icons, virtualization, or component-library wrappers), render the tree yourself using the outline data from onLoadSuccess or useOutlineContext.
No — react-pdf only fires onItemClick. You’re responsible for updating the page state (setCurrentPage) and, if you scroll-render pages, scrolling the right page into view. Some setups also call dest against the PDF.js link service for precise position (zoom + offset); for most apps, pageNumber is enough.