Add a Text-Based Watermark
Scenario
Section titled “Scenario”This example demonstrates how you can add a visible text watermark on top of every page in React PDF Kit without changing the original PDF file.
- Load a sample PDF
- Enable the default viewer toolbar
- Render a centered red watermark that says
CONFIDENTIAL - Demo Viewer
What to Use
Section titled “What to Use”The watermark works by reading the loaded document, registering a 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 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 watermark can detect the page count and apply an 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 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 overlay sizing stays aligned with the page. |
Code Example
Section titled “Code Example”-
Create the
CustomWatermarkcomponentsrc/components/CustomWatermark.jsx import { isValidElement, useEffect, useMemo, useRef } from "react";import {useDocumentContext,useElementPageContext,useZoomContext,} from "@react-pdf-kit/viewer";const WATERMARK_KEY = "custom-watermark";const DEFAULT_WATERMARK = {enabled: true,opacity: 0.18,color: "#1f2937",fontSize: 48,rotation: -32,placement: "center",repeat: false,};const REPEATED_WATERMARK_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 normalizeColor(value) {if (!value) {return DEFAULT_WATERMARK.color;}if (typeof CSS !== "undefined" && CSS.supports("color", value)) {return value;}return DEFAULT_WATERMARK.color;}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 getScaledFontSize(fontSize, pageWidth) {if (!pageWidth || !Number.isFinite(pageWidth) || pageWidth <= 0) {return fontSize;}return clamp(fontSize * (pageWidth / REFERENCE_PAGE_WIDTH),10,220,fontSize,);}function isWatermarkElement(element) {return isValidElement(element) && element.key === WATERMARK_KEY;}function withoutManagedWatermark(elements) {// Keep any custom page elements owned by the viewer or other features.return (elements ?? []).filter((element) => !isWatermarkElement(element));}export function CustomWatermark({enabled = DEFAULT_WATERMARK.enabled,content,opacity,color,fontSize,rotation,placement = DEFAULT_WATERMARK.placement,repeat = DEFAULT_WATERMARK.repeat,}) {const { pdf } = useDocumentContext();const { updateElement } = useElementPageContext();const { currentZoom } = useZoomContext();const managedPagesRef = useRef(new Set());const config = useMemo(() => {const normalizedContent = content.trim();// Normalize user input once so the page update effect only works with safe values.return {enabled: enabled && normalizedContent.length > 0,content: normalizedContent,opacity: clamp(opacity, 0.05, 0.8, DEFAULT_WATERMARK.opacity),color: normalizeColor(color),fontSize: clamp(fontSize, 12, 160, DEFAULT_WATERMARK.fontSize),rotation: clamp(rotation, -180, 180, DEFAULT_WATERMARK.rotation),placement,repeat,};}, [color, content, enabled, fontSize, opacity, placement, repeat, rotation]);useEffect(() => {const pageCount = pdf?.numPages ?? 0;const currentPages = new Set(Array.from({ length: pageCount }, (_, index) => index + 1),);const pagesToClear = new Set([...managedPagesRef.current, ...currentPages]);// Remove stale overlays when the document is unavailable or watermarking is disabled.if (!config.enabled || pageCount === 0) {pagesToClear.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedWatermark(previousElements),);});managedPagesRef.current.clear();return;}currentPages.forEach((pageNumber) => {// updateElement receives the current page dimensions, rotation, and zoom.// Use those values to size the overlay without mutating the PDF itself.updateElement(pageNumber, (previousElements, dimension, pageRotation, zoomLevel) => {const pageWidth = dimension?.width;const pageHeight = dimension?.height;const scaledFontSize = getScaledFontSize(config.fontSize, pageWidth);const watermark = (<divaria-hidden="true"className={`custom-watermark ${config.repeat ? "custom-watermark--repeat" : ""}`}data-page-rotation={pageRotation}data-zoom-level={zoomLevel}key={WATERMARK_KEY}style={{"--custom-watermark-color": config.color,"--custom-watermark-font-size": `${scaledFontSize}px`,"--custom-watermark-opacity": config.opacity,"--custom-watermark-rotation": `${config.rotation}deg`,width: pageWidth ? `${pageWidth}px` : "100%",height: pageHeight ? `${pageHeight}px` : "100%",}}>{config.repeat ? (<div className="custom-watermark__pattern">{REPEATED_WATERMARK_CELLS.map((cell) => (<span key={cell}>{config.content}</span>))}</div>) : (<spanclassName="custom-watermark__text"style={getPlacementStyle(config.placement)}>{config.content}</span>)}</div>);return [...withoutManagedWatermark(previousElements),watermark,];});});managedPagesRef.current = currentPages;return () => {currentPages.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedWatermark(previousElements),);});};}, [config, currentZoom, pdf?.numPages, updateElement]);return null;}src/components/CustomWatermark.tsx import {isValidElement,useEffect,useMemo,useRef,type CSSProperties,type ReactElement,} from "react";import {useDocumentContext,useElementPageContext,useZoomContext,} from "@react-pdf-kit/viewer";export type WatermarkPlacement =| "center"| "top-left"| "top-right"| "bottom-left"| "bottom-right";export interface CustomWatermarkProps {enabled?: boolean;content: string;opacity?: number;color?: string;fontSize?: number;rotation?: number;placement?: WatermarkPlacement;repeat?: boolean;}const WATERMARK_KEY = "custom-watermark";const DEFAULT_WATERMARK = {enabled: true,opacity: 0.18,color: "#1f2937",fontSize: 48,rotation: -32,placement: "center" as WatermarkPlacement,repeat: false,};const REPEATED_WATERMARK_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 normalizeColor(value: string | undefined) {if (!value) {return DEFAULT_WATERMARK.color;}if (typeof CSS !== "undefined" && CSS.supports("color", value)) {return value;}return DEFAULT_WATERMARK.color;}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 getScaledFontSize(fontSize: number, pageWidth?: number) {if (!pageWidth || !Number.isFinite(pageWidth) || pageWidth <= 0) {return fontSize;}return clamp(fontSize * (pageWidth / REFERENCE_PAGE_WIDTH),10,220,fontSize,);}function isWatermarkElement(element: HTMLElement | ReactElement) {return isValidElement(element) && element.key === WATERMARK_KEY;}function withoutManagedWatermark(elements?: Array<HTMLElement | ReactElement>) {// Keep any custom page elements owned by the viewer or other features.return (elements ?? []).filter((element) => !isWatermarkElement(element));}export function CustomWatermark({enabled = DEFAULT_WATERMARK.enabled,content,opacity,color,fontSize,rotation,placement = DEFAULT_WATERMARK.placement,repeat = DEFAULT_WATERMARK.repeat,}: CustomWatermarkProps) {const { pdf } = useDocumentContext();const { updateElement } = useElementPageContext();const { currentZoom } = useZoomContext();const managedPagesRef = useRef<Set<number>>(new Set());const config = useMemo(() => {const normalizedContent = content.trim();// Normalize user input once so the page update effect only works with safe values.return {enabled: enabled && normalizedContent.length > 0,content: normalizedContent,opacity: clamp(opacity, 0.05, 0.8, DEFAULT_WATERMARK.opacity),color: normalizeColor(color),fontSize: clamp(fontSize, 12, 160, DEFAULT_WATERMARK.fontSize),rotation: clamp(rotation, -180, 180, DEFAULT_WATERMARK.rotation),placement,repeat,};}, [color, content, enabled, fontSize, opacity, placement, repeat, rotation]);useEffect(() => {const pageCount = pdf?.numPages ?? 0;const currentPages = new Set(Array.from({ length: pageCount }, (_, index) => index + 1),);const pagesToClear = new Set([...managedPagesRef.current, ...currentPages]);// Remove stale overlays when the document is unavailable or watermarking is disabled.if (!config.enabled || pageCount === 0) {pagesToClear.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedWatermark(previousElements),);});managedPagesRef.current.clear();return;}currentPages.forEach((pageNumber) => {// updateElement receives the current page dimensions, rotation, and zoom.// Use those values to size the overlay without mutating the PDF itself.updateElement(pageNumber, (previousElements, dimension, pageRotation, zoomLevel) => {const pageWidth = dimension?.width;const pageHeight = dimension?.height;const scaledFontSize = getScaledFontSize(config.fontSize, pageWidth);const watermark = (<divaria-hidden="true"className={`custom-watermark ${config.repeat ? "custom-watermark--repeat" : ""}`}data-page-rotation={pageRotation}data-zoom-level={zoomLevel}key={WATERMARK_KEY}style={{"--custom-watermark-color": config.color,"--custom-watermark-font-size": `${scaledFontSize}px`,"--custom-watermark-opacity": config.opacity,"--custom-watermark-rotation": `${config.rotation}deg`,width: pageWidth ? `${pageWidth}px` : "100%",height: pageHeight ? `${pageHeight}px` : "100%",} as CSSProperties}>{config.repeat ? (<div className="custom-watermark__pattern">{REPEATED_WATERMARK_CELLS.map((cell) => (<span key={cell}>{config.content}</span>))}</div>) : (<spanclassName="custom-watermark__text"style={getPlacementStyle(config.placement)}>{config.content}</span>)}</div>);return [...withoutManagedWatermark(previousElements),watermark,];});});managedPagesRef.current = currentPages;return () => {currentPages.forEach((pageNumber) => {updateElement(pageNumber, (previousElements) =>withoutManagedWatermark(previousElements),);});};}, [config, currentZoom, pdf?.numPages, updateElement]);return null;} -
Add
CustomWatermarkinside the PDF viewer layoutsrc/App.jsx import { RPConfig, RPLayout, RPPages, RPProvider } from "@react-pdf-kit/viewer";import { CustomWatermark } from "./components/CustomWatermark";const pdfUrl ="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";const viewerName = "Demo Viewer";const watermarkContent = `CONFIDENTIAL - ${viewerName}`;function App() {return (<><RPConfig licenseKey={"YOUR_DOMAIN_TOKEN"}><RPProvider src={pdfUrl}><RPLayout toolbar><CustomWatermarkcolor="#e30613"content={watermarkContent}fontSize={42}opacity={0.2}placement="center"rotation={-32}/><RPPages /></RPLayout></RPProvider></RPConfig></>);}export default App;src/App.tsx import { RPConfig, RPLayout, RPPages, RPProvider } from "@react-pdf-kit/viewer";import { CustomWatermark } from "./components/CustomWatermark";const pdfUrl ="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";const viewerName = "Demo Viewer";const watermarkContent = `CONFIDENTIAL - ${viewerName}`;function App() {return (<><RPConfig licenseKey={"YOUR_DOMAIN_TOKEN"}><RPProvider src={pdfUrl}><RPLayout toolbar><CustomWatermarkcolor="#e30613"content={watermarkContent}fontSize={42}opacity={0.2}placement="center"rotation={-32}/><RPPages /></RPLayout></RPProvider></RPConfig></>);}export default App;
- Change
viewerNameorwatermarkContentto control the text shown in the watermark. - Change
color,fontSize,opacity,placement, androtationto adjust the text watermark appearance. CustomWatermarkmust be rendered inside theRPProviderand viewer layout so it can access document, page element, and zoom context.- The text watermark is a viewer overlay. It does not write the watermark into the original PDF, downloaded PDF, or printed output.
- If
contentis empty orenabledisfalse,CustomWatermarkremoves its managed watermark elements from the pages.