Skip to content
Add the searchTool and show search results in the sidebar

When viewing a PDF document, users need to search for text and see all search results displayed in a sidebar panel. Clicking on a result should navigate to that specific match location in the document.

The search panel should:

  • Allow users to enter a search keyword
  • Display all matches grouped by page
  • Navigate to a specific match when clicked
  • Highlight the currently selected match in the document

The useSearchContext hook provides functions to search text in a PDF document. This hook gives you access to the search state, results, and navigation functions to programmatically control the search experience.

Here are the key concepts we’re using for this example

NameObjective
searchDisplay the current search keyword in the input field
setSearchTrigger a search when the user submits a keyword
matchesDisplay search results grouped by page in the sidebar
loadingShow a loading indicator while searching
setCurrentMatchPositionJump to a specific match when user clicks a result
import {
RPProvider,
RPPages,
RPLayout,
RPVerticalBar,
RPHorizontalBar,
useSearchContext,
} from "@pdf-viewer/react";
import { useState, useEffect, useMemo } from "react";
import { createPortal } from "react-dom";
/**
* SearchInput Component
*
* A controlled input for entering search terms.
* Triggers search on Enter key press.
*/
const SearchInput = () => {
const { search, setSearch, loading } = useSearchContext();
const [inputValue, setInputValue] = useState(search);
const handleChange = (e) => {
setInputValue(e.target.value);
};
const handleKeyDown = (e) => {
if (e.key === "Enter") {
setSearch(inputValue);
}
};
return (
<div className="pdf-search-input-wrapper">
<input
type="text"
value={inputValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Search... (press Enter)"
className="pdf-search-input"
/>
{loading && <span className="pdf-search-loading">Searching...</span>}
</div>
);
};
/**
* SearchResults Component
*
* Displays search results grouped by page number.
* Each match can be clicked to navigate to that position in the PDF.
*/
const SearchResults = ({ matches }) => {
const { setCurrentMatchPosition } = useSearchContext();
// Group matches by page index
const groupedMatches = useMemo(() => {
const grouped = new Map();
matches.forEach((match, index) => {
const { pageIndex } = match;
if (!grouped.has(pageIndex)) {
grouped.set(pageIndex, []);
}
grouped.get(pageIndex).push({ matchIndex: index, match });
});
// Sort by page index and return as array
return Array.from(grouped.entries())
.sort(([a], [b]) => a - b)
.map(([pageIndex, pageMatches]) => ({ pageIndex, pageMatches }));
}, [matches]);
const handleNavigateToMatch = (matchIndex) => {
// setCurrentMatchPosition is 1-indexed
setCurrentMatchPosition(matchIndex + 1);
};
if (matches.length === 0) {
return null;
}
return (
<div className="pdf-search-results">
<div className="pdf-search-summary">
Found {matches.length} match{matches.length !== 1 ? "es" : ""}
</div>
{groupedMatches.map(({ pageIndex, pageMatches }) => (
<div key={pageIndex} className="pdf-search-page-group">
<div className="pdf-search-page-header">Page {pageIndex + 1}</div>
<div className="pdf-search-matches">
{pageMatches.map(({ matchIndex }) => (
<button
key={matchIndex}
className="pdf-search-match-btn"
onClick={() => handleNavigateToMatch(matchIndex)}
>
Match #{matchIndex + 1}
</button>
))}
</div>
</div>
))}
</div>
);
};
/**
* PdfSearchToolPanel Component
*
* A search panel that displays next to the sidebar using React Portal.
* Features:
* - Search input with Enter-to-search
* - Results grouped by page
* - Click-to-navigate functionality
*
* The panel is rendered as a sibling to [data-rp="sidebar"] element
* to position it correctly within the PDF viewer layout.
*/
const PdfSearchToolPanel = ({ visible }) => {
const { matches } = useSearchContext();
const [portalContainer, setPortalContainer] = useState(null);
// Find or create the portal container next to the sidebar
useEffect(() => {
if (!visible) {
return;
}
const sidebar = document.querySelector('[data-rp="sidebar"]');
if (!sidebar?.parentNode) {
return;
}
// Create container for the portal
const container = document.createElement("div");
container.className = "pdf-search-panel-container";
sidebar.parentNode.insertBefore(container, sidebar.nextSibling);
setPortalContainer(container);
// Cleanup on unmount or when hidden
return () => {
container.remove();
setPortalContainer(null);
};
}, [visible]);
if (!visible || !portalContainer) {
return null;
}
return createPortal(
<div className="pdf-search-panel">
<SearchInput />
<SearchResults matches={matches || []} />
</div>,
portalContainer
);
};
/**
* PdfSearchTool Component
*
* A custom search button for the PDF viewer sidebar that toggles a search panel.
* This demonstrates how to create a custom tool button that integrates with
* the PDF viewer's sidebar layout.
*/
const PdfSearchTool = () => {
const [isPanelVisible, setIsPanelVisible] = useState(false);
const togglePanel = () => setIsPanelVisible((prev) => !prev);
return (
<>
{/* Search toggle button styled to match the PDF viewer's theme */}
<button
className="pdf-viewer-btn"
onClick={togglePanel}
aria-label="Toggle search panel"
aria-expanded={isPanelVisible}
>
<svg
width="1em"
height="1em"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.9419 17.0577L14.0302 13.1468C15.1639 11.7856 15.7293 10.0398 15.6086 8.27238C15.488 6.50499 14.6906 4.85217 13.3823 3.65772C12.074 2.46328 10.3557 1.8192 8.58462 1.85944C6.81357 1.89969 5.12622 2.62118 3.87358 3.87383C2.62094 5.12647 1.89945 6.81382 1.8592 8.58486C1.81895 10.3559 2.46304 12.0743 3.65748 13.3825C4.85192 14.6908 6.50475 15.4882 8.27214 15.6089C10.0395 15.7295 11.7854 15.1642 13.1466 14.0304L17.0575 17.9421C17.1156 18.0002 17.1845 18.0463 17.2604 18.0777C17.3363 18.1091 17.4176 18.1253 17.4997 18.1253C17.5818 18.1253 17.6631 18.1091 17.739 18.0777C17.8149 18.0463 17.8838 18.0002 17.9419 17.9421C17.9999 17.8841 18.046 17.8151 18.0774 17.7392C18.1089 17.6634 18.125 17.5821 18.125 17.4999C18.125 17.4178 18.1089 17.3365 18.0774 17.2606C18.046 17.1848 17.9999 17.1158 17.9419 17.0577ZM3.12469 8.74993C3.12469 7.63741 3.45459 6.54988 4.07267 5.62485C4.69076 4.69982 5.56926 3.97885 6.5971 3.55311C7.62493 3.12737 8.75593 3.01598 9.84707 3.23302C10.9382 3.45006 11.9405 3.98579 12.7272 4.77246C13.5138 5.55913 14.0496 6.56141 14.2666 7.65255C14.4837 8.74369 14.3723 9.87469 13.9465 10.9025C13.5208 11.9304 12.7998 12.8089 11.8748 13.427C10.9497 14.045 9.86221 14.3749 8.74969 14.3749C7.25836 14.3733 5.82858 13.7801 4.77404 12.7256C3.71951 11.6711 3.12634 10.2413 3.12469 8.74993Z"
fill="currentColor"
/>
</svg>
</button>
{/* Search panel renders via portal next to sidebar */}
<PdfSearchToolPanel visible={isPanelVisible} />
</>
);
};
/**
* Custom left sidebar component that includes:
* - Thumbnail navigation tool
* - Custom search tool with results panel
*/
const OwnLeftSideBar = () => {
return (
<>
<RPVerticalBar slots={{ thumbnailTool: true }} />
<div>
<PdfSearchTool />
</div>
</>
);
};
/**
* PdfViewerSearchToolDemo
*
* Demonstrates how to add a custom search tool that displays
* search results in the sidebar. Key features:
* - Disables the default searchTool in the top bar
* - Adds a custom search button in the left sidebar
* - Shows search results grouped by page in a panel
* - Highlights matches on PDF pages
* - Allows navigation to specific matches
*/
const AppPdfViewer = () => {
return (
<RPProvider src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf">
<RPLayout
toolbar={{
topbar: { component: <RPHorizontalBar slots={{ searchTool: false }} /> },
leftSidebar: { component: <OwnLeftSideBar /> },
}}
>
<RPPages />
</RPLayout>
</RPProvider>
);
};
export default AppPdfViewer;