Mark text or areas for redaction, then permanently remove them from the document.
"use client";
import { createPluginRegistration } from "@embedpdf/core";About#
Redaction happens in two steps, and that's the format's design rather than an interface convention. A /Redact annotation (ISO 32000-1 §12.5.6.23, added in PDF 1.7) is a proposal: it marks a region and says what should replace it, and nothing is removed until a separate apply operation rewrites the page's content stream. So marking is reversible — a mark is a pending item you can clear or add to. Applying is not. The content underneath stops existing: it can't be copied, searched, or extracted afterwards.
<PdfRedactionToolbar /> covers both steps in one strip: a mode toggle for marking, a review popover over <PdfRedactionPanel />, a Clear button, and an Apply button that counts what's pending and confirms before it runs. It's an arrangement of three exported parts rather than a component of its own — <PdfRedactionModes />, the popover, and <PdfRedactionActions /> — so a surface with somewhere better to put the destructive half can take the marking half alone.
Redaction is a heavy-tier plugin, so <PdfViewer /> does not register it for you. The plugin and the layer that draws the marks both have to be added — see Usage.
Applying is irreversible and takes effect on the loaded document immediately. Keep the original file if you need one — this component gives you no way back.
Installation#
pnpm dlx shadcn@latest add @pdfviewcn/redaction-toolbar
Usage#
Register the annotation and redaction plugins.
import { createPluginRegistration } from "@embedpdf/core";
import { AnnotationPluginPackage } from "@embedpdf/plugin-annotation/react";
import { RedactionPluginPackage } from "@embedpdf/plugin-redaction/react";
// Outside the component — a new array identity on every render tears the
// engine down and rebuilds it, losing scroll and zoom state.
//
// Annotation has to be registered for `useAnnotationMode` to take — without it
// the redaction plugin warns and falls back to its own state. Array order isn't
// load-bearing; the registry sorts optional dependencies itself.
const plugins = [
createPluginRegistration(AnnotationPluginPackage),
createPluginRegistration(RedactionPluginPackage, {
drawBlackBoxes: true,
useAnnotationMode: true,
}),
];useAnnotationMode decides where a mark is kept — see Where marks are stored. drawBlackBoxes controls what applying leaves behind: true paints a black rectangle over the region, false removes the content and leaves the area blank. See embedpdf's Redaction plugin for its full configuration.
Render both layers and the toolbar.
<PdfViewer
documents={[{ url: "/sample.pdf" }]}
plugins={plugins}
className="h-[720px]"
>
<PdfToolbar>
<PdfRedactionToolbar />
</PdfToolbar>
<PdfViewerContent
pageLayers={({ documentId, pageIndex }) => (
<>
<PdfAnnotationLayer documentId={documentId} pageIndex={pageIndex} />
<RedactionLayer documentId={documentId} pageIndex={pageIndex} />
</>
)}
/>
</PdfViewer>Both layers are needed, and they draw different halves. In annotation mode a placed mark is an annotation, so <PdfAnnotationLayer /> is what draws it. <RedactionLayer /> draws the part that isn't an annotation yet — the box under the cursor mid-drag, the outline over a text selection before you let go. Drop it and marking becomes impossible to aim; drop the annotation layer and placed marks vanish the moment they land.
In legacy mode there are no annotations, so <RedactionLayer /> draws everything and the annotation layer isn't involved.
Where marks are stored#
The plugin has two storage modes, and the choice is useAnnotationMode on the registration rather than anything the toolbar arms. Both marking modes write through whichever one is configured.
Legacy is embedpdf's own name for the off state, not a deprecation — it's still the default. Every mark carries a source of 'annotation' or 'legacy', and the plugin says which path it's on in the console at startup.
useAnnotationMode: true | false (default) | |
|---|---|---|
| A mark is | a /Redact annotation (§12.5.6.23) | an entry in the plugin's own state |
| Survives a save | Yes | No |
| Undo/redo | With everything else | Not at all |
| Visible to other apps | Yes — it's in the file | No |
| Drawn by | <PdfAnnotationLayer /> | <RedactionLayer /> |
| Applied with | the engine's applyAllRedactions | the engine's redactTextInRects |
| Needs | the annotation plugin registered | nothing else |
Annotation mode is the spec-native path and the one to reach for: a mark that only exists in memory is one another application can't see and a save silently drops. It's off by default upstream so the plugin stays usable on its own, which is what legacy mode buys — redaction with no annotation plugin, in a viewer that applies in the same sitting.
useAnnotationMode: true without the annotation plugin registered doesn't
fail. The plugin warns once and runs legacy anyway, so marks that look correct
on screen are the ones a save drops.
Either way the destructive half is identical: applying rewrites the content stream through the engine, and storage doesn't change what's removed.
A /Redact annotation on a saved file is a marker, not a redaction. The
text under it is still in the file and still extractable until someone applies
it. That's the format's design — the annotation exists so the proposal can be
reviewed — but handing off a file with marks unapplied leaks exactly as much
as handing off the unmarked original.
Examples#
Default#
"use client";
import { createPluginRegistration } from "@embedpdf/core";Marking modes#
Two modes are offered, and they map to how the region is chosen rather than to what applying does:
- Mark text selects text the way a cursor does, and redacts what the selection covers. Use it when the content is text and you want the mark to follow the words.
- Mark area drags a rectangle, ignoring what's underneath. Use it for images, signatures, or scanned pages with no text layer.
The plugin has a third, unified mode that accepts both gestures at once. It's left out deliberately: it's a superset of these two rather than a tool of its own, and whether its marquee is live depends on the order the annotation and redaction plugins were registered in. Nothing is lost at the document level — where a mark is stored is settled by the registration, not by the armed mode.
Why a mark turns black on hover#
A pending mark is drawn by <RedactionLayer /> as an outline: a red border (/C, the annotation's stroke colour) over transparent content you can still read. Hover it and it fills solid black instead, with the border gone — and if /OverlayText is set, that text appears inside it.
That's not the mark being applied. It's a preview of what applying would leave behind: the fill is /IC, the interior colour the spec reserves for what replaces the region after the content stream is rewritten, and black is its default. Move the pointer away and the outline comes back with the text still underneath, because nothing has happened to the document.
The only thing that removes content is Apply, behind its confirmation dialog. If you can still see the text when the pointer moves off, the mark is still a proposal.
This preview is RedactHighlight's own behaviour inside @embedpdf/plugin-redaction, not something this registry draws, and it takes no props. Set /IC per mark to change what it shows.
Reviewing before applying#
The count on Apply says how many, not which. The button beside the modes opens <PdfRedactionPanel />, which groups every mark by page and quotes the text each one will destroy.
It's a popover here because reviewing is a step, not a mode: read it, remove what shouldn't be there, dismiss it. Give the panel a permanent rail instead when redaction is the whole job the surface is for.
Splitting the toolbar#
The whole strip is right when the host has nowhere else to put redaction. When it does, compose the parts instead — most usefully with <PdfRedactionBar />, which appears on its own the moment a mark exists:
<PdfToolbar>
<PdfRedactionModes />
</PdfToolbar>
<PdfRedactionBar />The marking half stays among the authoring controls; the destructive half moves to the banner that also says how many marks are about to be burned in. Mount both this toolbar and the bar and Apply exists twice, which is once too many for an action with no way back.
It's also what makes the toolbar safe to shed at a narrow width: the bar carries Clear and Apply on its own, so a document that arrives already marked stays reviewable even where nothing new can be marked.
Registering redaction on its own#
Redaction runs without the annotation plugin. Marks then live in the plugin's own state, drawn by <RedactionLayer /> alone — see Where marks are stored for what that costs:
const plugins = [
createPluginRegistration(RedactionPluginPackage, { drawBlackBoxes: true }),
];Nothing in the toolbar or the panel changes. Both read the plugin's pending state, which is the same shape either way.
When editing is disabled#
Applying a redaction rewrites the page contents, so the whole toolbar reads the active document's modify-contents permission and disables itself — with a tooltip explaining why — when the flag is denied. The demo below forces the flag off with a permissions override; a real document carries it in its own flags. See Permissions for the full model.
"use client";
import { createPluginRegistration } from "@embedpdf/core";Accessibility#
The mode toggle is a <ToggleGroup />, so arrow keys move between modes and the armed mode is exposed through its pressed state rather than colour alone. Both modes carry an aria-label matching their tooltip.
Applying is behind an <AlertDialog />, which moves focus to the dialog and traps it there until the user chooses — so a destructive action can't be triggered by a stray keypress on a focused button. The dialog's description states what is lost, not just that the action is permanent.
The review popover is keyboard-reachable and the panel inside it is fully navigable, which matters more here than usual: it's the only way to find out what a mark covers without hovering it on the page.
Marking a region requires a pointer; there is no keyboard equivalent for selecting text or dragging an area on the page.
API Reference#
PdfRedactionToolbar#
A div carrying data-slot="pdf-redaction-toolbar", arranging <PdfRedactionModes />, the review popover, and <PdfRedactionActions />. Accepts every div prop.
Returns null when the document isn't loaded or the redaction plugin isn't registered.
| Slot | Element |
|---|---|
pdf-redaction-toolbar | The wrapping row. |
pdf-redaction-modes | The mode toggle group. |
pdf-redaction-toolbar-item | Each mode toggle. |
pdf-redaction-review | Opens the pending list in a popover. |
pdf-redaction-actions | The Clear/Apply pair. |
pdf-redaction-clear | Discards every pending mark. |
pdf-redaction-apply | Opens the confirmation; shows the count. |
Review, Clear, and Apply are disabled while nothing is pending. Clear and Apply are also disabled while an apply is in flight — applying a long document rewrites every affected page, so Apply shows a spinner rather than appearing to do nothing.
PdfRedactionModes#
The marking half on its own: a div carrying data-slot="pdf-redaction-modes" around the two mode toggles, with the permission tooltip. Accepts every div prop.
Compose this instead of the whole toolbar when the destructive half lives elsewhere — see Splitting the toolbar.
Returns null when the document isn't loaded or the redaction plugin isn't registered.
PdfRedactionActions#
Clear and Apply, with the confirmation dialog. Exported from and documented on <PdfRedactionBar />.