Skip to content
Add a Text-Based Watermark

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

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.

NameObjective
pdfProvide 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.

NameObjective
updateElementAdd, 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.

NameObjective
currentZoomTrigger watermark updates when the viewer zoom changes so overlay sizing stays aligned with the page.
  1. Create the CustomWatermark component

    src/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 = (
    <div
    aria-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>
    ) : (
    <span
    className="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;
    }
  2. Add CustomWatermark inside the PDF viewer layout

    src/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>
    <CustomWatermark
    color="#e30613"
    content={watermarkContent}
    fontSize={42}
    opacity={0.2}
    placement="center"
    rotation={-32}
    />
    <RPPages />
    </RPLayout>
    </RPProvider>
    </RPConfig>
    </>
    );
    }
    export default App;
  • Change viewerName or watermarkContent to control the text shown in the watermark.
  • Change color, fontSize, opacity, placement, and rotation to adjust the text watermark appearance.
  • CustomWatermark must be rendered inside the RPProvider and 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 content is empty or enabled is false, CustomWatermark removes its managed watermark elements from the pages.