Skip to content
Customize a Search Popover

An image of customize a search popover

Create a custom search functionality with a popover interface in React PDF. The popover will include a search input field and additional options like “whole words” and “match case” filtering.

The useSearchContext hook provides access to the search functionality and configuration of the React PDF Viewer component. Use this hook to control search behavior and query updates.

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

NameObjective
setSearchFunction to update the search query
setSearchOptionsFunction to configure search behavior (wholeWords, matchCase)
currentMatchPositionTracks which match is currently highlighted
totalMatchesShows the total number of search results found
prevMatchFunction to go to the previous search result
nextMatchFunction to go to the next search result

To make the search popover experience smooth, you’ll want these 5 components:

Notes: This example will be using RPLayout with Individual Tools. Please refer to RPLayout for more information.

// Icon.tsx.
export const SearchIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 30 30">
<path d="M 13 3 C 7.4889971 3 3 7.4889971 3 13 C 3 18.511003 7.4889971 23 13 23 C 15.396508 23 17.597385 22.148986 19.322266 20.736328 L 25.292969 26.707031 A 1.0001 1.0001 0 1 0 26.707031 25.292969 L 20.736328 19.322266 C 22.148986 17.597385 23 15.396508 23 13 C 23 7.4889971 18.511003 3 13 3 z M 13 5 C 17.430123 5 21 8.5698774 21 13 C 21 17.430123 17.430123 21 13 21 C 8.5698774 21 5 17.430123 5 13 C 5 8.5698774 8.5698774 5 13 5 z"></path>
</svg>
);
};
export const MoreOptionsIcon = () => {
return (
<>
<svg
fill="#000000"
viewBox="0 0 32 32"
id="Outlined"
xmlns="http://www.w3.org/2000/svg"
>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
{" "}
<title></title>{" "}
<g id="Fill">
{" "}
<path d="M16,13a3,3,0,1,0,3,3A3,3,0,0,0,16,13Zm0,4a1,1,0,1,1,1-1A1,1,0,0,1,16,17Z"></path>{" "}
<path d="M24,13a3,3,0,1,0,3,3A3,3,0,0,0,24,13Zm0,4a1,1,0,1,1,1-1A1,1,0,0,1,24,17Z"></path>{" "}
<path d="M8,13a3,3,0,1,0,3,3A3,3,0,0,0,8,13Zm0,4a1,1,0,1,1,1-1A1,1,0,0,1,8,17Z"></path>{" "}
</g>{" "}
</g>
</svg>
</>
);
};
// SearchPopover.tsx
import { useSearchContext } from "@pdf-viewer/react";
import { useState } from "react";
import { MoreOptionsIcon } from "./Icon";
const SearchOptions = () => {
const { setSearchOptions } = useSearchContext();
return (
<div
style={{
position: "absolute",
minWidth: "max-content",
zIndex: 5,
top: "100%",
right: 0,
backgroundColor: "#f1f2f4",
padding: "8px",
borderRadius: "4px",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<input
type="checkbox"
onClick={() =>
setSearchOptions((val) => ({ ...val, wholeWords: !val.wholeWords }))
}
/>
<label>Whole words</label>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<input
type="checkbox"
onClick={() =>
setSearchOptions((val) => ({ ...val, matchCase: !val.matchCase }))
}
/>
<label>Matches case</label>
</div>
</div>
);
};
export const SearchPopover = () => {
const {
setSearch,
currentMatchPosition,
totalMatches,
prevMatch,
nextMatch,
} = useSearchContext();
const [isOpen, setIsOpen] = useState(false);
return (
<div
style={{
position: "absolute",
minWidth: "max-content",
zIndex: 5,
top: "100%",
right: 0,
backgroundColor: "#f1f2f4",
padding: "8px",
display: "flex",
borderRadius: "4px",
alignItems: "center",
gap: "4px",
}}
>
<input
placeholder="...Search"
style={{ height: "100%" }}
onChange={(e) => setSearch(e.target.value)}
/>
<span>
{currentMatchPosition} / {totalMatches}
</span>
<button onClick={prevMatch}>Prev</button>
<button onClick={nextMatch}>Next</button>
<button
style={{
position: "relative",
width: "20px",
height: "20px",
padding: "4px",
cursor: "pointer",
borderRadius: "4px",
border: 0,
}}
onClick={() => setIsOpen((prevValue) => !prevValue)}
>
<MoreOptionsIcon />
</button>
{isOpen && <SearchOptions />}
</div>
);
};
// CustomSearchTool.tsx
import { useState } from "react";
import { SearchIcon } from "./Icon";
import { SearchPopover } from "./SearchPopover";
export const CustomSearchTool = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
style={{
position: "relative",
}}
>
<button
style={{
position: "relative",
width: "28px",
height: "28px",
padding: "4px",
cursor: "pointer",
borderRadius: "4px",
border: 0,
}}
onClick={() => setIsOpen((prevValue) => !prevValue)}
>
<SearchIcon />
</button>
{isOpen && <SearchPopover />}
</div>
);
};
// CustomHorizonBar.tsx
import {
FileDownloadTool,
FileUploadTool,
FullScreenTool,
InputPageTool,
NextPageTool,
PreviousPageTool,
PrintTool,
ThemeSwitcherTool,
ZoomInTool,
ZoomLevelTool,
ZoomOutTool,
} from "@pdf-viewer/react";
import { CustomSearchTool } from "./CustomSearchTool";
export const CustomHorizonBar = () => {
return (
<>
<PreviousPageTool />
<InputPageTool />
<NextPageTool />
<div style={{ marginLeft: "auto" }}>
<ZoomInTool />
</div>
<ZoomLevelTool />
<ZoomOutTool />
<div style={{ marginLeft: "auto" }} />
<ThemeSwitcherTool />
<FileUploadTool />
<FileDownloadTool />
<PrintTool />
<FullScreenTool />
<CustomSearchTool />
</>
);
};
//Combine PDF Viewer and Custom Horizon Bar
import {
RPLayout,
RPPages,
RPProvider,
RPVerticalBar,
RPConfig,
} from "@pdf-viewer/react";
import { CustomHorizonBar } from "./CustomHorizonBar";
const App = () => {
return (
<div>
<RPConfig>
<RPProvider src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf">
<div>
<RPLayout
toolbar={{
topbar: { component: <CustomHorizonBar /> },
leftSidebar: { component: <RPVerticalBar /> },
}}
>
<RPPages />
</RPLayout>
</div>
</RPProvider>
</RPConfig>
</div>
);
};
export default App

Notes

  • The CustomHorizonBar component uses local state (isOpen) to control the visibility of the search popover
  • The SearchPopover component also maintains its own state for the options menu visibility
  • The zIndex: 5 ensures popovers appear above other content