PDF Stamp

Pick a rubber stamp from a library and click a page to place it.

"use client";

import { createPluginRegistration } from "@embedpdf/core";

About

The front end for embedpdf's stamp plugin, which owns the workflow — loading a library of rubber stamps, arming placement of one, and rendering the placed stamp through the annotation layer. This ships as one component with its parts exported:

  • <PdfStampLibrary /> — one library's stamps, each previewed with the plugin's own <StampImg />. Picking one arms placement (forDocument(documentId).activateStampPlacement(libraryId, stamp)); the next click on a page drops it in.
  • <PdfStampPalette /> — every loaded library, scrolling, with no container of its own. Registered with no config, the plugin loads a standard set (Approved, Draft, Confidential, and more) from its CDN manifest, so the palette is populated out of the box.
  • <PdfStampDialog /> — the palette in a modal, because the next click belongs to the page.
  • <PdfStampButton /> — the toolbar toggle that drives the dialog. It stays pressed while a placement is armed, and clicking it then cancels.

Every part reads its own scope off the plugin store rather than through a provider of ours, so any of them mounts alone.

Placed stamps render through the annotation plugin's <PdfAnnotationLayer> — the stamp plugin registers a rubber-stamp tool there — so both plugins have to be registered and a layer rendered. That tool is also how a pending placement is cancelled: the stamp capability has no deactivate of its own, so the button clears the annotation plugin's active tool instead.

Installation

pnpm dlx shadcn@latest add @pdfviewcn/stamp

Usage

Register the annotation and stamp plugins.

import { createPluginRegistration } from "@embedpdf/core";
import { AnnotationPluginPackage } from "@embedpdf/plugin-annotation/react";
import { StampPluginPackage } from "@embedpdf/plugin-stamp/react";
 
// Outside the component — a new array identity on every render tears the
// engine down and rebuilds it, losing scroll and zoom state.
const plugins = [
  createPluginRegistration(AnnotationPluginPackage),
  createPluginRegistration(StampPluginPackage),
];

Render the annotation layer — placed stamps render through it — and drop the button in the toolbar.

<PdfViewer
  documents={[{ url: "/sample.pdf" }]}
  plugins={plugins}
  className="h-[720px]"
>
  <PdfToolbar>
    <PdfStampButton />
  </PdfToolbar>
  <PdfViewerContent
    pageLayers={({ documentId, pageIndex }) => (
      <PdfAnnotationLayer documentId={documentId} pageIndex={pageIndex} />
    )}
  />
</PdfViewer>

Examples

Default

Open the button to see the stamp palette, pick a stamp to arm placement, then click a page to drop it in. Click the button again while it's pressed to cancel placement.

"use client";

import { createPluginRegistration } from "@embedpdf/core";

Your own trigger

<PdfStampDialog /> takes a trigger through children, so the toolbar toggle isn't the only way in.

<PdfStampDialog>
  <DialogTrigger render={<Button variant="outline" />}>Stamp</DialogTrigger>
</PdfStampDialog>

Your own container

The dialog is a convenience, not a requirement. Mount <PdfStampPalette /> in a popover, a sheet, or a sidebar panel instead — it brings no container of its own.

<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>Stamp</PopoverTrigger>
  <PopoverContent className="w-80">
    <PdfStampPalette />
  </PopoverContent>
</Popover>

One library

<PdfStampLibrary /> takes a single library, so a palette can show only the company's own stamps and leave the standard set out.

function CompanyStamps() {
  const { libraries } = useStampLibraries();
  const company = libraries.find((library) => library.id === "company");
 
  if (!company) return null;
 
  return <PdfStampLibrary library={company} />;
}

Accessibility

The trigger is a single toggle button labelled "Stamp", with a tooltip to match. Its pressed state reflects whether a placement is armed, and the tooltip switches to explain that clicking a page places the stamp — or clicking the button cancels.

Placing a stamp requires a pointer, since the drop point is chosen by clicking a page; there's no keyboard equivalent for picking a spot. The palette itself is keyboard reachable — each stamp is a button focusable in order, named by its subject. A stamp's preview is a bitmap of its page and carries no text of its own, so that name is the card's only one: it goes on aria-label and in a tooltip, rather than in a title a keyboard would never surface.

API Reference

PdfStampButton

A <Toggle /> carrying data-slot="pdf-stamp-button", plus the dialog it drives. Accepts every prop the underlying toggle takes; pressed, onPressedChange, aria-label, and the default icon are managed internally but can be overridden.

PropTypeDefaultDescription
openbooleanWhether the stamp dialog is open.
defaultOpenbooleanfalseInitial open state. Uncontrolled.
onOpenChange(open: boolean, eventDetails?) => voidCalled when the dialog opens or closes.

The button reads the stamp capability through useStampCapability() and the armed placement through useActiveStamp(documentId). It renders disabled whenever there's no active document or the stamp plugin isn't registered.

PdfStampDialog

<PdfStampPalette /> in a modal, because the next click belongs to the page rather than to the dialog. Picking a stamp arms placement and closes. Accepts every <Dialog /> prop; children is the trigger slot.

onOpenChange carries Base UI's eventDetails as an optional second argument — optional because closing on a pick happens from the inside, where there's no DOM event to attribute the change to.

PdfStampPalette

Every loaded library inside a <ScrollArea /> carrying data-slot="pdf-stamp-palette". Shows a loading line until the CDN manifest arrives, since the standard stamps are fetched on mount. Accepts every <ScrollArea /> prop.

PropTypeDefaultDescription
onSelect(libraryId: string, stamp: StampDefinition) => voidForwarded to every <PdfStampLibrary />.

PdfStampLibrary

One library's stamps as a labelled pick-to-place grid, carrying data-slot="pdf-stamp-library".

PropTypeDefaultDescription
libraryStampLibraryOne library from useStampLibraries(). Required.
onSelect(libraryId: string, stamp: StampDefinition) => voidReplaces the default, which arms placement for the picked stamp.