PDF Redaction Bar

A banner that appears only while a document carries unapplied redaction marks, and the one place applying them happens.

"use client";

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

About

A /Redact annotation (ISO 32000-1 §12.5.6.23) is a proposal. It marks a region and says what should replace it, and nothing is removed until a separate apply pass rewrites the page's content stream. So a marked document is unfinished, and the danger is someone sending it on believing the marks did the work.

<PdfRedactionBar /> is the answer to that. It counts the pending marks, carries Clear and Apply, and mounts and unmounts with the marks themselves: nothing pending, nothing on screen. It arrives on its own rather than waiting in a panel to be found.

It's deliberately not a list. In annotation mode the plugin's pending set is a projection over the annotation store — every /Redact in it — so the marks are already named and listed in <PdfAnnotationSidebar />. The bar reports the count and lets the host route there rather than duplicating that list.

Redaction is a heavy-tier plugin, so <PdfViewer /> does not register it for you — see Usage.

Installation

pnpm dlx shadcn@latest add @pdfviewcn/redaction-bar

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,
  }),
];

Mount the bar where a banner belongs, and give the toolbar the marking modes.

<PdfViewer documents={[{ url: "/sample.pdf" }]} plugins={plugins}>
  <PdfToolbar>
    <PdfRedactionModes />
  </PdfToolbar>
 
  <PdfRedactionBar />
 
  <PdfViewerContent
    pageLayers={({ documentId, pageIndex }) => (
      <>
        <PdfAnnotationLayer documentId={documentId} pageIndex={pageIndex} />
        <RedactionLayer documentId={documentId} pageIndex={pageIndex} />
      </>
    )}
  />
</PdfViewer>

<PdfRedactionModes /> is the marking half of <PdfRedactionToolbar /> on its own. Compose that rather than the whole toolbar when the bar is mounted: the toolbar carries an Apply of its own, and the irreversible action shouldn't be offered twice.

Put the bar full-bleed under the toolbar and above the content — it renders with no top or side borders so it reads as a property of the document rather than a card floating over it.

Structure

<PdfRedactionBar>{/* host actions, before Clear and Apply */}</PdfRedactionBar>

Anything passed as children lands before the actions — that's where a host puts its own route into the marks. <PdfRedactionActions /> is appended after it, and is exported separately if you want the pair somewhere else entirely.

Examples

Default

"use client";

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

Routing to the marks

The count says how many, not which. Since applying is the one action with no way back, give the bar a way into the list — in annotation mode that's the annotation sidebar, where every /Redact already appears under its own name:

<PdfRedactionBar>
  <Button
    variant="ghost"
    size="sm"
    onClick={() => {
      setTab("annotations");
      setPanelOpen(true);
    }}
  >
    Review
  </Button>
</PdfRedactionBar>

This is what <PdfAnnotatorApp /> does. In legacy storage mode marks aren't annotations and don't appear in the sidebar at all, so point the button at <PdfRedactionPanel /> instead — see Where marks are stored.

The actions on their own

<PdfRedactionActions /> is the Clear/Apply pair, including the confirmation dialog and its copy:

<PdfRedactionActions />

Use it directly when you have your own banner, a footer, or a menu to put the pair in. It renders both buttons at all times, disabled while nothing is pending — unlike the bar, which isn't there at all.

When editing is disabled

Applying rewrites the page contents, so both actions read the active document's modify-contents permission and disable themselves when the flag is denied. The bar still appears — marks still exist and still need reporting — and anchors a tooltip explaining why nothing responds. See Permissions for the full model.

Accessibility

The bar overrides <Alert />'s default to role="status". Its content changes with every mark made, and alert would interrupt a screen reader on each one; status announces the running count politely.

Applying is behind an <AlertDialog />, 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.

Because the bar mounts only while marks are pending, its buttons enter and leave the tab order with it. Nothing else in the surface shifts focus when it appears.

API Reference

PdfRedactionBar

An <Alert /> carrying data-slot="pdf-redaction-bar", holding the count, any children, and <PdfRedactionActions />. Accepts every <Alert /> prop.

Returns null when the document isn't loaded, the redaction plugin isn't registered, or nothing is pending.

PropTypeDefaultDescription
documentIdstring | nullambient documentThe document to report on. Defaults to the focused view's active document.
childrenReactNodeRendered before the actions — where a host puts its own route into the marks.

PdfRedactionActions

A div carrying data-slot="pdf-redaction-actions", holding Clear and Apply. Accepts every div prop, plus the same documentId.

Returns null when the document isn't loaded or the redaction plugin isn't registered.

SlotElement
pdf-redaction-barThe banner.
pdf-redaction-actionsThe action pair.
pdf-redaction-clearDiscards every pending mark.
pdf-redaction-applyOpens the confirmation; shows the count.

Both actions are disabled while nothing is pending, while editing is disallowed, and 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.

There's no per-mark apply — redaction is reviewed as a set and applied as a set. See What isn't here.