Skip to content
Add an Image-Based Watermark

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.

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.

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

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

NameObjective
currentZoomTrigger watermark updates when the viewer zoom changes so the image overlay stays aligned and proportional to the page.
  1. 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 = (
    <div
    aria-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>
    ) : (
    <img
    alt={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;
    }
  2. 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>
    <CustomImageWatermark
    opacity={0.2}
    placement="center"
    rotation={0}
    size={150}
    src="YOUR_IMAGE"
    />
    <RPPages />
    </RPLayout>
    </RPProvider>
    </RPConfig>
    );
    }
  3. For a repeated logo watermark, set repeat to true

    <CustomImageWatermark
    opacity={0.12}
    repeat
    rotation={-28}
    size={120}
    src="/react-pdf-kit-logo.png"
    />
  4. 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 CustomImageWatermark when the watermark should be a logo, seal, brand mark, or repeated image instead of text.
  • Set alt for a single meaningful logo or seal. When repeat is enabled, the repeated images are decorative and use empty alt text.
  • Change opacity, size, placement, and rotation to adjust the image watermark appearance.
  • Set repeat to true when you want a repeated image pattern across each page instead of one positioned image.
  • The component scales size from the rendered page width and reruns when currentZoom changes, so the watermark remains proportional while zooming.
  • CustomImageWatermark must be rendered inside RPProvider and RPLayout so it can access document, page element, and zoom context.
  • If src is empty or enabled is false, 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.