PDF Redaction Panel

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.

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

MarkRow reads
Over textThe 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.

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.

PropTypeDefaultDescription
documentIdstringThe ambient documentWhich document's marks to list.
headingReact.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.

SlotElement
pdf-redaction-panelThe wrapping column.
pdf-redaction-panel-headerThe title row and count.
pdf-redaction-panel-pageOne page's section.
pdf-redaction-panel-rowOne 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.