Draw freehand annotations directly on PDF documents using mouse or touch input.
Drawing highlights (type: "drawing") enable users to:
- Draw freehand lines and shapes on PDF pages
- Customize stroke color and width
- Drag drawings to reposition them
- Resize while maintaining aspect ratio
Drawings are stored as PNG images, making them compatible with PDF export.
<PdfHighlighter
enableDrawingCreation={() => drawingMode}
onDrawingComplete={handleDrawingComplete}
drawingConfig={{
strokeColor: "#ff0000",
strokeWidth: 2,
}}
highlights={highlights}
>
<HighlightContainer />
</PdfHighlighter>const [drawingMode, setDrawingMode] = useState(false);
const handleDrawingComplete = (position: ScaledPosition, dataUrl: string) => {
const newHighlight: Highlight = {
id: generateId(),
type: "drawing",
position,
content: { image: dataUrl },
};
setHighlights([newHighlight, ...highlights]);
setDrawingMode(false);
};import { DrawingHighlight, useHighlightContainerContext } from "react-pdf-highlighter-extended";
const HighlightContainer = ({ editHighlight }) => {
const { highlight, viewportToScaled, isScrolledTo, highlightBindings } = useHighlightContainerContext();
const { toggleEditInProgress } = usePdfHighlighterContext();
if (highlight.type === "drawing") {
return (
<DrawingHighlight
highlight={highlight}
isScrolledTo={isScrolledTo}
bounds={highlightBindings.textLayer}
onChange={(boundingRect) => {
editHighlight(highlight.id, {
position: {
boundingRect: viewportToScaled(boundingRect),
rects: [],
},
});
}}
onEditStart={() => toggleEditInProgress(true)}
onEditEnd={() => toggleEditInProgress(false)}
/>
);
}
// ... handle other highlight types
};| Prop | Type | Description |
|---|---|---|
enableDrawingCreation |
(event: MouseEvent) => boolean |
Returns true when drawing mode is active |
onDrawingComplete |
(position: ScaledPosition, dataUrl: string) => void |
Called when user finishes drawing |
drawingConfig |
DrawingConfig |
Stroke color and width settings |
interface DrawingConfig {
strokeColor?: string; // Default: "#000000"
strokeWidth?: number; // Default: 2
}| Prop | Type | Required | Description |
|---|---|---|---|
highlight |
ViewportHighlight |
Yes | The highlight data (must have content.image) |
onChange |
(rect: LTWHP) => void |
No | Called when position/size changes |
isScrolledTo |
boolean |
No | Whether highlight was auto-scrolled to |
bounds |
string | Element |
No | Bounds for dragging (react-rnd) |
onContextMenu |
(event: MouseEvent) => void |
No | Right-click handler |
onEditStart |
() => void |
No | Called when drag/resize begins |
onEditEnd |
() => void |
No | Called when drag/resize ends |
style |
CSSProperties |
No | Custom container styling |
dragIcon |
ReactNode |
No | Custom drag handle icon |
.DrawingHighlight { } /* Container */
.DrawingHighlight__container { } /* Inner wrapper */
.DrawingHighlight__toolbar { } /* Toolbar (hidden by default, shows on hover) */
.DrawingHighlight__drag-handle { } /* Drag handle icon */
.DrawingHighlight__content { } /* Drawing container */
.DrawingHighlight__image { } /* The drawing image */
.DrawingHighlight--scrolledTo { } /* When auto-scrolled to */- Toolbar appears on hover: The drag handle is hidden until you hover over the drawing
- Aspect ratio locked: Resizing maintains the original proportions
- Minimum size: 50x50 pixels
- Transparent background: Drawings have transparent backgrounds for clean overlay
import React, { useState, useRef } from "react";
import {
PdfHighlighter,
PdfHighlighterUtils,
PdfLoader,
DrawingHighlight,
TextHighlight,
AreaHighlight,
useHighlightContainerContext,
usePdfHighlighterContext,
ScaledPosition,
Highlight,
} from "react-pdf-highlighter-extended";
const App = () => {
const [highlights, setHighlights] = useState<Highlight[]>([]);
const [drawingMode, setDrawingMode] = useState(false);
const [strokeColor, setStrokeColor] = useState("#ff0000");
const [strokeWidth, setStrokeWidth] = useState(2);
const highlighterUtilsRef = useRef<PdfHighlighterUtils>();
const handleDrawingComplete = (position: ScaledPosition, dataUrl: string) => {
const newHighlight: Highlight = {
id: String(Math.random()).slice(2),
type: "drawing",
position,
content: { image: dataUrl },
};
setHighlights([newHighlight, ...highlights]);
setDrawingMode(false);
};
const editHighlight = (id: string, edit: Partial<Highlight>) => {
setHighlights(
highlights.map((h) => (h.id === id ? { ...h, ...edit } : h))
);
};
return (
<div>
{/* Toolbar */}
<div className="toolbar">
<button
onClick={() => setDrawingMode(!drawingMode)}
className={drawingMode ? "active" : ""}
>
{drawingMode ? "Exit Drawing" : "Draw"}
</button>
{drawingMode && (
<>
<label>
Color:
<input
type="color"
value={strokeColor}
onChange={(e) => setStrokeColor(e.target.value)}
/>
</label>
<label>
Width:
<select
value={strokeWidth}
onChange={(e) => setStrokeWidth(Number(e.target.value))}
>
<option value={1}>Thin (1px)</option>
<option value={2}>Normal (2px)</option>
<option value={4}>Thick (4px)</option>
<option value={8}>Extra Thick (8px)</option>
</select>
</label>
</>
)}
</div>
<PdfLoader document="https://example.com/document.pdf">
{(pdfDocument) => (
<PdfHighlighter
pdfDocument={pdfDocument}
highlights={highlights}
utilsRef={(utils) => (highlighterUtilsRef.current = utils)}
enableDrawingCreation={() => drawingMode}
onDrawingComplete={handleDrawingComplete}
drawingConfig={{
strokeColor,
strokeWidth,
}}
>
<HighlightContainer editHighlight={editHighlight} />
</PdfHighlighter>
)}
</PdfLoader>
</div>
);
};
// Highlight Container Component
const HighlightContainer = ({
editHighlight,
}: {
editHighlight: (id: string, edit: Partial<Highlight>) => void;
}) => {
const {
highlight,
viewportToScaled,
isScrolledTo,
highlightBindings,
} = useHighlightContainerContext();
const { toggleEditInProgress } = usePdfHighlighterContext();
if (highlight.type === "drawing") {
return (
<DrawingHighlight
highlight={highlight}
isScrolledTo={isScrolledTo}
bounds={highlightBindings.textLayer}
onChange={(boundingRect) => {
editHighlight(highlight.id, {
position: {
boundingRect: viewportToScaled(boundingRect),
rects: [],
},
});
}}
onEditStart={() => toggleEditInProgress(true)}
onEditEnd={() => toggleEditInProgress(false)}
/>
);
}
if (highlight.type === "text") {
return <TextHighlight highlight={highlight} isScrolledTo={isScrolledTo} />;
}
// Area highlight (default)
return (
<AreaHighlight
highlight={highlight}
isScrolledTo={isScrolledTo}
bounds={highlightBindings.textLayer}
/>
);
};
export default App;- Click the "Draw" button to enter drawing mode
- Optionally adjust stroke color and width
- Click and drag on the PDF to draw
- Release to complete the drawing
- Drawing is saved and exits drawing mode
- Hover over the drawing to reveal the drag handle
- Click and drag the handle icon (6-dot grid)
- Release to save new position
- Hover over the drawing corners/edges
- Drag to resize (aspect ratio is maintained)
- Release to save new size
Drawing highlights use the standard Highlight interface with type: "drawing":
interface Highlight {
id: string;
type: "drawing";
position: ScaledPosition;
content?: {
image?: string; // Base64 PNG data URL
};
}The content.image field contains a base64-encoded PNG data URL of the drawing with transparent background.
Drawings are fully supported in PDF export. They are embedded as PNG images at their exact position and size on the page.
import { exportPdf } from "react-pdf-highlighter-extended";
const pdfBytes = await exportPdf(pdfUrl, highlights);
// Drawings are automatically included- Thin strokes (1-2px): Good for writing and fine details
- Medium strokes (3-4px): Good for underlining and circling
- Thick strokes (6-8px): Good for emphasis and highlighting
- Use distinct colors for different annotation purposes
- Keep drawings simple for better readability
- Use smaller stroke widths for detailed annotations
- Drawings with transparent backgrounds overlay cleanly on PDF content