A powerful React library for annotating PDF documents
Text highlights β’ Area highlights β’ Freetext notes β’ Images & signatures β’ Freehand drawing β’ Shapes β’ Search β’ PDF export
react-pdf-highlighter-plus provides a highly customizable annotation experience for PDF documents in React applications. Built on PDF.js, it stores highlight positions in viewport-independent coordinates, making them portable across different screen sizes.
| Feature | Description |
|---|---|
| Text Highlights | Select and highlight text passages, restyle them, and copy their text |
| Area Highlights | Draw rectangular regions on PDFs and copy intersecting PDF text |
| Freetext Notes | Draggable, editable sticky notes with custom styling and compact mode |
| Images & Signatures | Upload images or draw signatures directly on PDFs |
| Freehand Drawing | Draw freehand annotations with customizable stroke |
| Shapes | Add rectangles, circles, and arrows with editable stroke style |
| PDF Search | Search through all PDF text with next/previous navigation |
| PDF Export | Export annotated PDF with all highlights embedded |
| Local PDF Worker | Uses the packaged PDF.js worker by default |
| Light/Dark Theme | Hue-preserving dark mode (OKLab recolor) β photos & colors stay readable |
| Zoom Support | Buttons, pinch / ctrl+wheel zoom, position-independent data |
| Smooth Scroll | Animated scroll-to-highlight (respects reduced-motion) |
| Deep Linking | initialPage + onPageChange for ?page=N style navigation |
| Locate Text | getTextPosition β turn any quote into a precise highlight (citations) |
| Read Aloud | Text-to-speech that highlights & follows each sentence (recipe) |
| Fast Loading | Progressive range loading, auth headers, document caching |
| Fully Customizable | Exposed styling on all components |
| Resource | Link |
|---|---|
| Live Demo | View Demo |
| Documentation | API Docs |
| NPM Package | npm |
npm install react-pdf-highlighter-plusimport "react-pdf-highlighter-plus/style/style.css";PDF.js worker setup is handled by the package by default. The build copies the local pdf.worker.min.mjs into the package output, so most apps do not need to configure workerSrc manually.
import {
PdfLoader,
PdfHighlighter,
TextHighlight,
AreaHighlight,
useHighlightContainerContext,
} from "react-pdf-highlighter-plus";
import "react-pdf-highlighter-plus/style/style.css";
function App() {
const [highlights, setHighlights] = useState([]);
return (
<PdfLoader document="https://example.com/document.pdf">
{(pdfDocument) => (
<PdfHighlighter
pdfDocument={pdfDocument}
highlights={highlights}
enableAreaSelection={(e) => e.altKey}
>
<HighlightContainer />
</PdfHighlighter>
)}
</PdfLoader>
);
}
function HighlightContainer() {
const { highlight, isScrolledTo } = useHighlightContainerContext();
return highlight.type === "text" ? (
<TextHighlight highlight={highlight} isScrolledTo={isScrolledTo} />
) : (
<AreaHighlight highlight={highlight} isScrolledTo={isScrolledTo} />
);
}Select text in the PDF to create highlights.
<TextHighlight
highlight={highlight}
isScrolledTo={isScrolledTo}
style={{ background: "rgba(255, 226, 143, 1)" }}
/>Text and area highlight toolbars include a copy button. After copying, the icon changes to a check mark for 1.5 seconds.
Hold Alt and drag to create rectangular highlights.
<PdfHighlighter
enableAreaSelection={(event) => event.altKey}
// ...
>Create draggable, editable text annotations with customizable styling.
import { FreetextHighlight } from "react-pdf-highlighter-plus";
<PdfHighlighter
enableFreetextCreation={() => freetextMode}
onFreetextClick={(position) => {
addHighlight({ type: "freetext", position, content: { text: "Note" } });
}}
>
// In your highlight container:
<FreetextHighlight
highlight={highlight}
onChange={handlePositionChange}
onTextChange={handleTextChange}
onStyleChange={handleStyleChange}
color="#333333"
backgroundColor="#ffffc8"
fontSize="14px"
/>Features:
- Drag to reposition
- Click to edit text
- Built-in style panel (colors, font size, font family)
- Toolbar appears on hover
Upload images or draw signatures and place them on PDFs.
import { ImageHighlight, SignaturePad } from "react-pdf-highlighter-plus";
// Signature pad modal
<SignaturePad
isOpen={isOpen}
onComplete={(dataUrl) => setPendingImage(dataUrl)}
onClose={() => setIsOpen(false)}
/>
// In your highlight container:
<ImageHighlight
highlight={highlight}
onChange={handlePositionChange}
onEditStart={() => toggleEditInProgress(true)}
onEditEnd={() => toggleEditInProgress(false)}
/>Features:
- Upload any image format
- Draw signatures with mouse or touch
- Drag to reposition
- Resize while maintaining aspect ratio
- Toolbar appears on hover
Draw freehand annotations directly on PDFs.
import { DrawingHighlight } from "react-pdf-highlighter-plus";
<PdfHighlighter
enableDrawingMode={drawingMode}
onDrawingComplete={(dataUrl, position, strokes) => {
addHighlight({ type: "drawing", position, content: { image: dataUrl } });
}}
drawingStrokeColor="#ff0000"
drawingStrokeWidth={2}
>
// In your highlight container:
<DrawingHighlight
highlight={highlight}
onChange={handlePositionChange}
/>Features:
- Freehand drawing with mouse or touch
- Customizable stroke color and width
- Stored as PNG for PDF export compatibility
- Drag to reposition
Create rectangle, circle, and arrow annotations with editable stroke color and width.
<PdfHighlighter
enableShapeMode={shapeMode} // "rectangle" | "circle" | "arrow" | null
onShapeComplete={(position, shape) => {
addHighlight({ type: "shape", position, content: { shape } });
}}
shapeStrokeColor="#000000"
shapeStrokeWidth={2}
>
<HighlightContainer />
</PdfHighlighter>Shape geometry stays in the normal highlight layer while the style controls render in the higher config layer.
Use utilsRef to access document-wide search helpers backed by PDF.js PDFFindController.
const highlighterUtilsRef = useRef<PdfHighlighterUtils>();
<PdfHighlighter
pdfDocument={pdfDocument}
highlights={highlights}
utilsRef={(utils) => (highlighterUtilsRef.current = utils)}
>
<HighlightContainer />
</PdfHighlighter>
highlighterUtilsRef.current?.search("TypeScript", {
highlightAll: true,
caseSensitive: false,
});
highlighterUtilsRef.current?.findNext();
highlighterUtilsRef.current?.clearSearch();Dark mode recolors each page at render time using a hue-preserving OKLab map β
white paper maps to a dark background and black text to a light foreground, while
colors keep their hue and embedded photos keep their pixels (unlike a CSS
invert() filter). Highlights, the text selection, and the left panel all adapt.
// Enable dark mode (warm-gray default palette)
<PdfHighlighter
pdfDocument={pdfDocument}
theme={{ mode: "dark" }}
highlights={highlights}
>
<HighlightContainer />
</PdfHighlighter>
// Customize the dark palette
<PdfHighlighter
pdfDocument={pdfDocument}
theme={{
mode: "dark",
darkModeColors: {
background: "#141210", // replaces white paper
foreground: "#eae6e0", // replaces black text / line-art
},
containerBackgroundColor: "#3a3a3a",
scrollbarThumbColor: "#6b6b6b",
scrollbarTrackColor: "#2c2c2c",
}}
highlights={highlights}
>
<HighlightContainer />
</PdfHighlighter>Highlights:
- Hue-preserving recolor (red stays red, blue links stay blue); photos untouched.
- Highlights stay readable: translucent fill + a border, with no
mix-blendwash-out. - Scroll and zoom are preserved when toggling the theme.
LeftPanelacceptsmode="dark"so the outline/thumbnails panel matches.- Drawing/shape default ink becomes white in dark mode.
Deprecated:
theme.darkModeInvertIntensityis ignored β dark mode no longer uses a CSSinvert()filter. Usetheme.darkModeColorsinstead.
PdfLoader loads documents progressively and caches them.
<PdfLoader
document="https://api.example.com/files/report.pdf"
// Fetch only the pages needed to render first (needs server HTTP range support)
disableAutoFetch={true}
// Auth / cross-origin
httpHeaders={{ Authorization: `Bearer ${token}` }}
withCredentials={false}
// Reuse the same URL instantly on remount / re-open (default true)
enableCache={true}
>
{(pdfDocument) => <PdfHighlighter pdfDocument={pdfDocument} /* β¦ */ />}
</PdfLoader>| Prop | Default | Description |
|---|---|---|
disableAutoFetch |
true |
Fetch pages on demand instead of the whole file (first page shows fast on range-capable servers) |
disableStream |
false |
Disable progressive streaming |
rangeChunkSize |
pdf.js | Size of each range request in bytes |
httpHeaders |
β | Extra request headers (e.g. an auth token) |
withCredentials |
false |
Send cookies with the request |
enableCache |
true |
Cache the loaded document by URL (also dedupes StrictMode double-mount) |
beforeLoad |
spinner | Render while loading; receives progress or null |
Progressive loading requires the server to support HTTP range requests (
Accept-Ranges: bytes) and exposeContent-Rangevia CORS.
<PdfHighlighter
pdfDocument={pdfDocument}
initialPage={12} // jump here on first load (deep-link)
onPageChange={(page) => syncUrl(page)} // current page changed
onZoomChange={(scale) => setZoom(scale)} // pinch / ctrl+wheel zoom changed
highlights={highlights}
>
<HighlightContainer />
</PdfHighlighter>- Pinch / ctrl(β)+wheel zoom is built in β smooth (GPU transform during the gesture, one crisp re-render on settle), anchored to the cursor.
scrollToHighlight(highlight)(fromusePdfHighlighterContext) now scrolls smoothly and respectsprefers-reduced-motion.initialPageis applied once on load;onPageChangefires as the visible page changes β wire them to a?page=NURL for deep links.
getTextPosition finds a piece of text in the PDF and returns a precise
ScaledPosition β turn an external quote (e.g. an AI citation) into a highlight
you can render or scroll to. Matching ignores whitespace/line-wraps and falls
back to fuzzy matching.
import { getTextPosition } from "react-pdf-highlighter-plus";
const match = await getTextPosition(pdfDocument, "the exact or near-exact quote");
if (match) {
const citation = {
id: "cite-1",
type: "text",
content: { text: match.matchedText },
position: match.position, // precise rects, page-independent
};
setHighlights((prev) => [citation, ...prev]);
utils.scrollToHighlight(citation); // smooth scroll + flash
}
// match: { position, pageNumber, matchedText, confidence: "exact" | "fuzzy" }Build a read-aloud / "PDF to audio" experience: speak the document and
highlight + auto-scroll to each sentence as it's read. The pieces are already
here β extractSentences gives the ordered script (text + position) and
scrollToHighlight follows along. The TTS engine is yours to choose (the
browser speechSynthesis, or a cloud voice).
import { extractSentences } from "react-pdf-highlighter-plus";
// 1. Build the ordered script once.
const sentences = (
await extractSentences(pdfDocument, { includePositions: true })
).filter((s) => s.position);
// 2. Speak each sentence; highlight + scroll as it starts.
function readFrom(index: number) {
const s = sentences[index];
if (!s) return;
const reading = {
id: "reading",
type: "text",
content: { text: s.text },
position: s.position!,
};
setHighlights((prev) => [reading, ...prev.filter((h) => h.id !== "reading")]);
utils.scrollToHighlight(reading); // smooth follow
const utter = new SpeechSynthesisUtterance(s.text);
utter.onend = () => readFrom(index + 1); // advance
speechSynthesis.speak(utter);
}
readFrom(0);Swap speechSynthesis for any engine β sentence-level sync needs no word
timing. See the example app for a full transport (play/pause/seek/speed/voice).
Export your annotated PDF with all highlights embedded.
import { exportPdf } from "react-pdf-highlighter-plus";
const handleExport = async () => {
const pdfBytes = await exportPdf(pdfUrl, highlights, {
textHighlightColor: "rgba(255, 226, 143, 0.5)",
areaHighlightColor: "rgba(255, 226, 143, 0.5)",
onProgress: (current, total) => console.log(`${current}/${total} pages`),
});
// Download the file
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "annotated.pdf";
a.click();
URL.revokeObjectURL(url);
};Supported highlight types:
- Text highlights (colored rectangles)
- Area highlights (colored rectangles)
- Freetext notes (background + wrapped text)
- Images & signatures (embedded PNG/JPG)
- Freehand drawings (embedded PNG)
- Shapes (rectangle, circle, arrow)
The package renders React annotation components into PDF.js page overlay layers:
| Layer | Mount point | Purpose |
|---|---|---|
.PdfHighlighter__highlight-layer |
Inside PDF.js .textLayer |
Annotation geometry for text, area, image/signature, drawing, and shape |
.PdfHighlighter__note-layer |
Direct child of PDF.js .page |
Freetext notes and compact note markers above PDF content |
.PdfHighlighter__config-layer |
Direct child of PDF.js .page |
Toolbars, style panels, copy buttons, and controls above PDF content |
Internal flow diagram:
| Hook | Purpose |
|---|---|
usePdfHighlighterContext() |
Viewer utilities: scrollToHighlight, setTip, getCurrentSelection |
useHighlightContainerContext() |
Per-highlight utilities: highlight, viewportToScaled, screenshot |
The library uses two coordinate systems:
| System | Description | Use Case |
|---|---|---|
| Viewport | Pixel coordinates relative to current zoom | Rendering on screen |
| Scaled | Normalized (0-1) coordinates relative to page | Storage & retrieval |
// Converting between systems
const { viewportToScaled } = useHighlightContainerContext();
// Save position (viewport β scaled)
const scaledPosition = viewportToScaled(boundingRect);
// Highlights are automatically converted to viewport when renderinginterface MyHighlight extends Highlight {
category: string;
comment?: string;
author?: string;
}
// Use the generic type
const { highlight } = useHighlightContainerContext<MyHighlight>();// Via props
<TextHighlight
highlight={highlight}
style={{ background: categoryColors[highlight.category] }}
/>
// Via CSS classes
.TextHighlight { }
.AreaHighlight { }
.FreetextHighlight { }
.ImageHighlight { }
.DrawingHighlight { }import { MonitoredHighlightContainer } from "react-pdf-highlighter-plus";
<MonitoredHighlightContainer
highlightTip={{
position: highlight.position,
content: <MyPopup highlight={highlight} />,
}}
>
<TextHighlight highlight={highlight} />
</MonitoredHighlightContainer>git clone https://github.com/QuocVietHa08/react-pdf-highlighter-plus.git
cd react-pdf-highlighter-plus
npm install
npm run devSee the full API Reference for detailed documentation on all components and types.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
For bugs, please open an issue with clear reproduction steps.
MIT
Originally forked from react-pdf-highlighter with significant architectural changes including context-based APIs, zoom support, freetext/image/drawing highlights, and PDF export functionality.
