Add an Image-Based Watermark
Scenario
Section titled “Scenario”This example shows how to render an image overlay on every page in React PDF Kit without changing the original PDF file.
- Load a sample PDF
- Enable the default viewer toolbar
- Render an image on each PDF page
This approach is useful when the watermark needs to communicate visually:
- A company wants every previewed document to show its logo.
- A legal or finance workflow needs a seal or stamp on reviewed files.
- A document portal needs a repeated background pattern to discourage screenshots.
What to Use
Section titled “What to Use”The image watermark works by reading the loaded document, registering a managed custom element on each rendered page, and responding when the viewer zoom changes.
Start with useDocumentContext to access the current PDF document state provided by RPProvider. The image watermark needs the loaded document before it can decide which pages should receive an overlay.
| Name | Objective |
|---|---|
pdf | Provide the loaded PDF document so the component can detect the page count and apply an image overlay to each page. |
Then use useElementPageContext to manage custom React elements inside individual PDF pages.
| Name | Objective |
|---|---|
updateElement | Add, replace, or remove the managed image watermark element for each page. |
Finally, use useZoomContext to react to viewer zoom changes that affect page dimensions and overlay sizing.
| Name | Objective |
|---|---|
currentZoom | Trigger watermark updates when the viewer zoom changes so the image overlay stays aligned and proportional to the page. |
Code Example
Section titled “Code Example”-
Create an image watermark component
src/components/CustomImageWatermark.jsx import { isValidElement, useEffect, useMemo, useRef } from "react";import {useDocumentContext,useElementPageContext,useZoomContext,} from "@react-pdf-kit/viewer";const IMAGE_WATERMARK_KEY = "custom-image-watermark";const DEFAULT_IMAGE_WATERMARK = {enabled: true,// Replace this with a public image path, imported asset URL, or remote image URL.src: "YOUR_IMAGE",alt: "",opacity: 0.16,size: 150,rotation: -28,placement: "center",repeat: false,};const REPEATED_IMAGE_CELLS = Array.from({ length: 9 }, (_, index) => index);const REFERENCE_PAGE_WIDTH = 720;function clamp(value, min, max, fallback) {return typeof value === "number" && Number.isFinite(value)? Math.min(Math.max(value, min), max): fallback;}function getPlacementStyle(placement) {switch (placement) {case "top-left":return { top: "12%", left: "12%", transformOrigin: "center" };case "top-right":return { top: "12%", right: "12%", transformOrigin: "center" };case "bottom-left":return { bottom: "12%", left: "12%", transformOrigin: "center" };case "bottom-right":return { right: "12%", bottom: "12%", transformOrigin: "center" };case "center":default:return {top: "50%",left: "50%",transformOrigin: "center",translate: "-50% -50%",};}}function getScaledSize(size, pageWidth) {if (!pageWidth || !Number.isFinite(pageWidth) || pageWidth <= 0) {return size;}// Scale the image relative to page width so it feels consistent across zoomed pages.return clamp(size * (pageWidth / REFERENCE_PAGE_WIDTH), 32, 320, size);}function isImageWatermarkElement(element) {return isValidElement(element) && element.key === IMAGE_WATERMARK_KEY;}function withoutManagedImageWatermark(elements) {// Preserve page elements from the viewer and from other custom overlays.return (elements ?? []).filter((element) => !isImageWatermarkElement(element));}export function CustomImageWatermark({enabled = DEFAULT_IMAGE_WATERMARK.enabled,src = DEFAULT_IMAGE_WATERMARK.src,alt = DEFAULT_IMAGE_WATERMARK.alt,opacity,size,rotation,placement = DEFAULT_IMAGE_WATERMARK.placement,repeat = DEFAULT_IMAGE_WATERMARK.repeat,}) {const { pdf } = useDocumentContext();const { updateElement } = useElementPageContext();const { currentZoom } = useZoomContext();const managedPagesRef = useRef(new Set());const config = useMemo(() => {const normalizedSrc = src.trim();// Normalize input before updating page elements so invalid values fall back safely.return {enabled: enabled && normalizedSrc.length > 0,src: normalizedSrc,alt,opacity: clamp(opacity, 0.05, 0.8, DEFAULT_IMAGE_WATERMARK.opacity),size: clamp(size, 32, 320, DEFAULT_IMAGE_WATERMARK.size),rotation: clamp(rotation, -180, 180, DEFAULT_IMAGE_WATERMARK.rotation),placement,repeat,};}, [alt, enabled, opacity, placement, repeat, rotation, size, src]);useEffect(() => {const pageCount = pdf?.numPages ?? 0;const currentPages = new Set(Array.from({ length: pageCount }, (_, index) => index + 1),);const pagesToClear = new Set([...managedPagesRef.current, ...currentPages]);// Clear stale image overlays when the PDF is not loaded or the watermark is disabled.if (!config.enabled || pageCount === 0) {pagesToClear.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedImageWatermark(previousElements),);});managedPagesRef.current.clear();return;}currentPages.forEach((pageNumber) => {// updateElement gives access to page dimensions, rotation, and zoom for this page.updateElement(pageNumber, (previousElements, dimension, pageRotation, zoomLevel) => {const pageWidth = dimension?.width;const pageHeight = dimension?.height;const scaledSize = getScaledSize(config.size, pageWidth);const image = (<divaria-hidden="true"className={`custom-image-watermark ${config.repeat ? "custom-image-watermark--repeat" : ""}`}data-page-rotation={pageRotation}data-zoom-level={zoomLevel}key={IMAGE_WATERMARK_KEY}style={{"--custom-image-watermark-opacity": config.opacity,"--custom-image-watermark-rotation": `${config.rotation}deg`,"--custom-image-watermark-size": `${scaledSize}px`,width: pageWidth ? `${pageWidth}px` : "100%",height: pageHeight ? `${pageHeight}px` : "100%",}}>{config.repeat ? (// Repeated watermarks are decorative, so each image uses empty alt text.<div className="custom-image-watermark__pattern">{REPEATED_IMAGE_CELLS.map((cell) => (<img alt="" key={cell} src={config.src} />))}</div>) : (<imgalt={config.alt}className="custom-image-watermark__image"src={config.src}style={getPlacementStyle(config.placement)}/>)}</div>);return [...withoutManagedImageWatermark(previousElements),image,];});});managedPagesRef.current = currentPages;return () => {currentPages.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedImageWatermark(previousElements),);});};}, [config, currentZoom, pdf?.numPages, updateElement]);return null;}src/components/CustomImageWatermark.tsx import {isValidElement,useEffect,useMemo,useRef,type CSSProperties,type ReactElement,} from "react";import {useDocumentContext,useElementPageContext,useZoomContext,} from "@react-pdf-kit/viewer";import type { WatermarkPlacement } from "./CustomWatermark";export interface CustomImageWatermarkProps {enabled?: boolean;src?: string;alt?: string;opacity?: number;size?: number;rotation?: number;placement?: WatermarkPlacement;repeat?: boolean;}const IMAGE_WATERMARK_KEY = "custom-image-watermark";const DEFAULT_IMAGE_WATERMARK = {enabled: true,// Replace this with a public image path, imported asset URL, or remote image URL.src: "YOUR_IMAGE",alt: "",opacity: 0.16,size: 150,rotation: -28,placement: "center" as WatermarkPlacement,repeat: false,};const REPEATED_IMAGE_CELLS = Array.from({ length: 9 }, (_, index) => index);const REFERENCE_PAGE_WIDTH = 720;function clamp(value: number | undefined, min: number, max: number, fallback: number) {return typeof value === "number" && Number.isFinite(value)? Math.min(Math.max(value, min), max): fallback;}function getPlacementStyle(placement: WatermarkPlacement): CSSProperties {switch (placement) {case "top-left":return { top: "12%", left: "12%", transformOrigin: "center" };case "top-right":return { top: "12%", right: "12%", transformOrigin: "center" };case "bottom-left":return { bottom: "12%", left: "12%", transformOrigin: "center" };case "bottom-right":return { right: "12%", bottom: "12%", transformOrigin: "center" };case "center":default:return {top: "50%",left: "50%",transformOrigin: "center",translate: "-50% -50%",};}}function getScaledSize(size: number, pageWidth?: number) {if (!pageWidth || !Number.isFinite(pageWidth) || pageWidth <= 0) {return size;}// Scale the image relative to page width so it feels consistent across zoomed pages.return clamp(size * (pageWidth / REFERENCE_PAGE_WIDTH), 32, 320, size);}function isImageWatermarkElement(element: HTMLElement | ReactElement) {return isValidElement(element) && element.key === IMAGE_WATERMARK_KEY;}function withoutManagedImageWatermark(elements?: Array<HTMLElement | ReactElement>) {// Preserve page elements from the viewer and from other custom overlays.return (elements ?? []).filter((element) => !isImageWatermarkElement(element));}export function CustomImageWatermark({enabled = DEFAULT_IMAGE_WATERMARK.enabled,src = DEFAULT_IMAGE_WATERMARK.src,alt = DEFAULT_IMAGE_WATERMARK.alt,opacity,size,rotation,placement = DEFAULT_IMAGE_WATERMARK.placement,repeat = DEFAULT_IMAGE_WATERMARK.repeat,}: CustomImageWatermarkProps) {const { pdf } = useDocumentContext();const { updateElement } = useElementPageContext();const { currentZoom } = useZoomContext();const managedPagesRef = useRef<Set<number>>(new Set());const config = useMemo(() => {const normalizedSrc = src.trim();// Normalize input before updating page elements so invalid values fall back safely.return {enabled: enabled && normalizedSrc.length > 0,src: normalizedSrc,alt,opacity: clamp(opacity, 0.05, 0.8, DEFAULT_IMAGE_WATERMARK.opacity),size: clamp(size, 32, 320, DEFAULT_IMAGE_WATERMARK.size),rotation: clamp(rotation, -180, 180, DEFAULT_IMAGE_WATERMARK.rotation),placement,repeat,};}, [alt, enabled, opacity, placement, repeat, rotation, size, src]);useEffect(() => {const pageCount = pdf?.numPages ?? 0;const currentPages = new Set(Array.from({ length: pageCount }, (_, index) => index + 1),);const pagesToClear = new Set([...managedPagesRef.current, ...currentPages]);// Clear stale image overlays when the PDF is not loaded or the watermark is disabled.if (!config.enabled || pageCount === 0) {pagesToClear.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedImageWatermark(previousElements),);});managedPagesRef.current.clear();return;}currentPages.forEach((pageNumber) => {// updateElement gives access to page dimensions, rotation, and zoom for this page.updateElement(pageNumber, (previousElements, dimension, pageRotation, zoomLevel) => {const pageWidth = dimension?.width;const pageHeight = dimension?.height;const scaledSize = getScaledSize(config.size, pageWidth);const image = (<divaria-hidden="true"className={`custom-image-watermark ${config.repeat ? "custom-image-watermark--repeat" : ""}`}data-page-rotation={pageRotation}data-zoom-level={zoomLevel}key={IMAGE_WATERMARK_KEY}style={{"--custom-image-watermark-opacity": config.opacity,"--custom-image-watermark-rotation": `${config.rotation}deg`,"--custom-image-watermark-size": `${scaledSize}px`,width: pageWidth ? `${pageWidth}px` : "100%",height: pageHeight ? `${pageHeight}px` : "100%",} as CSSProperties}>{config.repeat ? (// Repeated watermarks are decorative, so each image uses empty alt text.<div className="custom-image-watermark__pattern">{REPEATED_IMAGE_CELLS.map((cell) => (<img alt="" key={cell} src={config.src} />))}</div>) : (<imgalt={config.alt}className="custom-image-watermark__image"src={config.src}style={getPlacementStyle(config.placement)}/>)}</div>);return [...withoutManagedImageWatermark(previousElements),image,];});});managedPagesRef.current = currentPages;return () => {currentPages.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedImageWatermark(previousElements),);});};}, [config, currentZoom, pdf?.numPages, updateElement]);return null;} -
Add the image watermark inside the PDF viewer layout
src/App.jsx import { RPConfig, RPLayout, RPPages, RPProvider } from "@react-pdf-kit/viewer";import { CustomImageWatermark } from "./components/CustomImageWatermark";function App() {return (<RPConfig licenseKey={"YOUR_DOMAIN_TOKEN"}><RPProvider src={pdfUrl}><RPLayout toolbar><CustomImageWatermarkopacity={0.2}placement="center"rotation={0}size={150}src="YOUR_IMAGE"/><RPPages /></RPLayout></RPProvider></RPConfig>);}src/App.tsx import { RPConfig, RPLayout, RPPages, RPProvider } from "@react-pdf-kit/viewer";import { CustomImageWatermark } from "./components/CustomImageWatermark";function App() {return (<RPConfig licenseKey={"YOUR_DOMAIN_TOKEN"}><RPProvider src={pdfUrl}><RPLayout toolbar><CustomImageWatermarkopacity={0.2}placement="center"rotation={0}size={150}src="YOUR_IMAGE"/><RPPages /></RPLayout></RPProvider></RPConfig>);} -
For a repeated logo watermark, set
repeattotrue<CustomImageWatermarkopacity={0.12}repeatrotation={-28}size={120}src="/react-pdf-kit-logo.png"/><CustomImageWatermarkopacity={0.12}repeatrotation={-28}size={120}src="/react-pdf-kit-logo.png"/> -
Add the image watermark styles
.custom-image-watermark {position: absolute;inset: 0;overflow: hidden;pointer-events: none;user-select: none;z-index: 2;}.custom-image-watermark__image {position: absolute;width: var(--custom-image-watermark-size);height: var(--custom-image-watermark-size);object-fit: contain;opacity: var(--custom-image-watermark-opacity);transform: rotate(var(--custom-image-watermark-rotation));}.custom-image-watermark__pattern {display: grid;width: 100%;height: 100%;grid-template-columns: repeat(3, minmax(0, 1fr));grid-template-rows: repeat(3, minmax(0, 1fr));place-items: center;}.custom-image-watermark__pattern img {width: calc(var(--custom-image-watermark-size) * 0.72);height: calc(var(--custom-image-watermark-size) * 0.72);object-fit: contain;opacity: var(--custom-image-watermark-opacity);transform: rotate(var(--custom-image-watermark-rotation));}
- Use
CustomImageWatermarkwhen the watermark should be a logo, seal, brand mark, or repeated image instead of text. - Set
altfor a single meaningful logo or seal. Whenrepeatis enabled, the repeated images are decorative and use empty alt text. - Change
opacity,size,placement, androtationto adjust the image watermark appearance. - Set
repeattotruewhen you want a repeated image pattern across each page instead of one positioned image. - The component scales
sizefrom the rendered page width and reruns whencurrentZoomchanges, so the watermark remains proportional while zooming. CustomImageWatermarkmust be rendered insideRPProviderandRPLayoutso it can access document, page element, and zoom context.- If
srcis empty orenabledisfalse, the component removes its managed watermark elements from the pages. - The image watermark is a viewer overlay. It does not write the image into the original PDF, downloaded PDF, or printed output.