Every mark waiting on the document, grouped by page, with the text each one will destroy.
"use client";
import { createPluginRegistration } from "@embedpdf/core";About#
A redaction is proposed before it happens. A /Redact annotation (ISO 32000-1 §12.5.6.23) marks a region and says what should replace it; nothing is removed until a separate apply operation rewrites the page's content stream.
<PdfRedactionPanel /> lists what's pending, grouped by page, each mark named by the text it covers — so what's about to go can be read while it's still readable. "3 marks" tells you how many, not which, and on the page a text mark and an area mark are both a red box. Selecting a row selects the mark and scrolls to it.
Reviewing is the last moment the content exists. Applying rewrites the pages and the text underneath stops being recoverable — by copying, searching, or extracting.
Installation#
pnpm dlx shadcn@latest add @pdfviewcn/redaction-panel
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.
const plugins = [
createPluginRegistration(AnnotationPluginPackage),
createPluginRegistration(RedactionPluginPackage, {
drawBlackBoxes: true,
useAnnotationMode: true,
}),
];Put the panel beside the page, with a toolbar to mark from.
<PdfViewer
documents={[{ url: "/sample.pdf" }]}
plugins={plugins}
className="h-[720px]"
>
<PdfToolbar>
<PdfRedactionToolbar />
</PdfToolbar>
<div className="flex min-h-0 flex-1">
<PdfRedactionPanel className="w-64 shrink-0 border-r" />
<PdfViewerContent
className="flex-1"
pageLayers={({ documentId, pageIndex }) => (
<>
<PdfAnnotationLayer documentId={documentId} pageIndex={pageIndex} />
<RedactionLayer documentId={documentId} pageIndex={pageIndex} />
</>
)}
/>
</div>
</PdfViewer>The panel reads the plugin's pending state, which is the same shape in both of the plugin's storage modes — so it works unchanged with redaction registered on its own. See Where marks are stored.
What each row says#
| Mark | Row reads |
|---|---|
| Over text | The text it covers |
| Over text, none | "Marked text" |
| Over an area | "Marked area" |
An area mark covers whatever is drawn under it — an image, a signature, a scanned page with no text layer — so it has nothing to quote. A text mark with nothing to quote is the same case arriving a different way: the region holds no extractable text, or the mark came from a document that recorded none with it.
A mark spanning several pages writes the whole selection's text to every page it touches, so the same passage can appear on two rows. That's the plugin's behaviour, not the panel's — the rows are two marks, and applying removes each page's own region.
Examples#
Default#
Mark text or an area with the toolbar; each mark lands in the panel. Click a row to select the mark and scroll to it.
"use client";
import { createPluginRegistration } from "@embedpdf/core";In a popover#
<PdfRedactionToolbar /> mounts the panel this way itself:
<PopoverContent align="start" className="w-72 overflow-hidden p-0">
<PdfRedactionPanel className="max-h-80 [--pdf-redaction-surface:var(--popover)]" />
</PopoverContent>--pdf-redaction-surface is what the sticky page headers sit on. It defaults to --background; point it at the host's own slab when that differs, or the headers band across the rows they sit over.
In a tabbed rail#
Pass heading={null} when the surrounding chrome already names the panel:
<TabsContent value="redactions" className="min-h-0">
<PdfRedactionPanel heading={null} className="h-full" />
</TabsContent>When editing is disabled#
Removing a mark edits the document, so each row's remove button reads the active document's modify-contents permission and disables when it's denied. The list stays readable either way. See Permissions for the full model.
Accessibility#
Each page is a <section> labelled Page 3, so the list is navigable by region rather than as one long run of rows.
A row is a button. The selected row carries aria-current, so selection is announced rather than only tinted — the mark's own colour is never the only signal.
Remove buttons fade in on hover, but they're focusable throughout and pointer-events-none rides along with the fade, so tabbing to one makes it visible and a hidden one is never a live click target.
Marking a region requires a pointer; everything in the panel is keyboard-reachable once a mark exists.
API Reference#
PdfRedactionPanel#
A div carrying data-slot="pdf-redaction-panel". Accepts every div prop.
| Prop | Type | Default | Description |
|---|---|---|---|
documentId | string | The ambient document | Which document's marks to list. |
heading | React.ReactNode | null | "Redactions" | The panel's title. null drops the header when surrounding chrome already names it. |
Returns null when the document isn't loaded or the redaction plugin isn't registered. Renders an empty state when nothing is marked.
| Slot | Element |
|---|---|
pdf-redaction-panel | The wrapping column. |
pdf-redaction-panel-header | The title row and count. |
pdf-redaction-panel-page | One page's section. |
pdf-redaction-panel-row | One mark; carries data-redaction-id. |
What isn't here#
Apply, per mark or for all. Applying one mark and not its neighbours leaves a document where the two look identical — a red box either way — with no way to tell which text is gone. Redaction is reviewed as a set and applied as a set, so Apply and Clear live on the toolbar, behind the confirmation.
Editing a mark's region. A mark is dragged and resized on the page, where you can see what it covers. A panel can't show you that.