---
title: "Redaction — React"
description: "Mark text and areas for removal, review the marks, then remove the content for good."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/annotations/redaction"
---

# Redaction

Redaction removes content for good: the text, images and drawings under a mark are deleted from the
file, not just covered up. It happens in two steps, as in Acrobat: first people **mark** what should
go, and review it; then they **apply** the marks, and the content is gone.

Register `redactionPlugin()` next to `interactionPlugin()` and `annotationPlugin()`, and use the
`redact` tool: select text to mark it, or drag to mark an area:

**`bar.tsx`**

```tsx
import { useInteraction } from '@embedpdf/react/interaction';
import { useRedaction, useRedactionState } from '@embedpdf/react/redaction';

export function RedactBar() {
  const interaction = useInteraction();
  const redaction = useRedaction();
  const { pendingCount, applying } = useRedactionState();

  return (
    <>
      <button onClick={() => interaction.activateTool('redact')}>Mark for redaction</button>
      <button disabled={!pendingCount || applying} onClick={() => redaction.applyAll()}>
        Redact {pendingCount} marks
      </button>
    </>
  );
}
```

**`basic.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
  interactionPlugin,
  useInteraction,
  useInteractionState,
} from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/react/selection';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import { redactionPlugin, useRedaction, useRedactionState } from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';

import './basic.css';

const engine = localEngine();
// The selection plugin lets the redact tool mark the text you select.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin(),
  annotationPlugin(),
  redactionPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// The author's name on the cover, in page coordinates.
const AUTHOR = { x: 100, y: 508, width: 172, height: 50 };

function RedactBar() {
  const interaction = useInteraction();
  const { activeToolId } = useInteractionState();
  const stage = useStage();
  const redaction = useRedaction();
  const { pendingCount, applying } = useRedactionState();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const started = useRef(false);

  // On load: the author's name marked, and the redact tool on.
  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void redaction.markArea(0, AUTHOR).then(() => stage.reveal(0, { rect: AUTHOR }));
    interaction.activateTool('redact');
  }, [redaction, interaction, stage, ready]);

  const marking = activeToolId === 'redact';

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        aria-pressed={marking}
        onClick={() => interaction.activateTool(marking ? 'pointer' : 'redact')}
      >
        Mark for redaction
      </button>
      <button
        type="button"
        className="button danger"
        disabled={!pendingCount || applying}
        onClick={() => void redaction.applyAll()}
      >
        Redact {pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}
      </button>
      <span className="spacer" />
      <output className="readout">
        {marking ? 'Select text, or drag over an area' : 'Click a mark to move it'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <RedactBar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`basic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --danger: #c4262e;
  --danger-strong: #a81f26;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* The one button that can't be undone */
.button.danger {
  color: var(--surface);
  background: var(--danger);
  border-color: var(--danger);
}

.button.danger:hover {
  background: var(--danger-strong);
  border-color: var(--danger-strong);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

A mark is an annotation, so until it's applied, people can move it, resize it, delete it, and see it
in other PDF apps. Applying it is what removes the content.

## Mark from code

```ts
await redaction.markSelection(); // the selected text
await redaction.markArea(page.ref, { x: 72, y: 100, width: 200, height: 40 });
await redaction.markPage(page.ref); // the whole page
await redaction.markMatches({ text: 'Jane Doe' }); // every match in the document
await redaction.markMatches({ text: '\\d{3}-\\d{2}-\\d{4}', regex: true }); // like a social security number
```

`markMatches` takes the same query as [search](../text/search), and resolves with `{ marks }`, one per
match.

**`from-code.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin, useSelectionState } from '@embedpdf/react/selection';
import { searchPlugin } from '@embedpdf/react/search';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import { redactionPlugin, useRedaction, useRedactionState } from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';

import './from-code.css';

const engine = localEngine();
// Marking matches runs a search; marking the selected text needs a selection.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin(),
  searchPlugin(),
  annotationPlugin(),
  redactionPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

function MarkFromCode() {
  const redaction = useRedaction();
  const { pendingCount } = useRedactionState();
  const { hasSelection } = useSelectionState();
  const currentPage = useStageState((state) => state.currentPageIndex);
  const ready = useAnnotationState((state) => state.status === 'ready');
  const [text, setText] = useState('EmbedPDF');
  const started = useRef(false);

  // On load: every "EmbedPDF" in the document.
  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void redaction.markMatches({ text: 'EmbedPDF' });
  }, [redaction, ready]);

  return (
    <div className="toolbar">
      <form
        className="query"
        onSubmit={(event) => {
          event.preventDefault();
          if (text.trim()) void redaction.markMatches({ text: text.trim() });
        }}
      >
        <input
          className="field"
          type="search"
          aria-label="Text to mark"
          value={text}
          onChange={(event) => setText(event.target.value)}
        />
        <button type="submit" className="button" disabled={!text.trim()}>
          Every match
        </button>
      </form>
      <button
        type="button"
        className="button"
        disabled={!hasSelection}
        title="Select some text on the page first"
        onClick={() => void redaction.markSelection()}
      >
        The selected text
      </button>
      <button type="button" className="button" onClick={() => void redaction.markPage(currentPage)}>
        This page
      </button>
      <button
        type="button"
        className="button"
        disabled={!pendingCount}
        onClick={() => void redaction.clearPending()}
      >
        Remove all
      </button>
      <span className="spacer" />
      <output className="readout">
        {pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <MarkFromCode />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`from-code.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* A text box or a menu in the toolbar */
.field {
  appearance: none;
  min-width: 0;
  box-sizing: border-box;
  min-height: 34px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

select.field {
  appearance: auto;
  padding-right: 6px;
  cursor: pointer;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
}

/* The text to mark and its button, together */
.query {
  display: flex;
  flex: 0 1 280px;
  gap: 8px;
  min-width: 0;
}

.query .field {
  flex: 1 1 auto;
  width: 100%;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Review the marks

`usePendingRedactions()` lists the marks that haven't been applied yet, for a review panel:

**`pending.tsx`**

```tsx
import { annotationKey } from '@embedpdf/react/annotation';
import { usePendingRedactions, useRedaction } from '@embedpdf/react/redaction';
import { useStage } from '@embedpdf/react/stage';

export function Pending() {
  const stage = useStage();
  const redaction = useRedaction();
  const marks = usePendingRedactions();

  return (
    <ul>
      {marks.map((mark) => (
        <li key={annotationKey(mark.ref)}>
          <button onClick={() => stage.reveal(mark.page, { rect: mark.bounds })}>
            Page {mark.pageIndex + 1}: {mark.kind === 'text' ? 'text' : 'area'}
          </button>
          <button onClick={() => redaction.unmark([mark.ref])}>Remove</button>
        </li>
      ))}
    </ul>
  );
}
```

`redaction.clearPending()` removes every mark. `usePendingRedactions({ page: page.ref })`
lists one page's.

**`review.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { searchPlugin } from '@embedpdf/react/search';
import {
  AnnotationLayer,
  annotationKey,
  annotationPlugin,
  useAnnotationState,
} from '@embedpdf/react/annotation';
import {
  redactionPlugin,
  usePendingRedactions,
  useRedaction,
  useRedactionState,
} from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';

import './review.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  searchPlugin(),
  annotationPlugin(),
  redactionPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// On load: the author's name, and every "commercial" in the document.
function MarkSome() {
  const redaction = useRedaction();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const started = useRef(false);

  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void redaction
      .markArea(0, { x: 100, y: 508, width: 172, height: 50 })
      .then(() => redaction.markMatches({ text: 'commercial' }));
  }, [redaction, ready]);

  return null;
}

function PendingMarks() {
  const stage = useStage();
  const redaction = useRedaction();
  const marks = usePendingRedactions(); // the marks not applied yet, in page order
  const { pendingCount } = useRedactionState();

  return (
    <div className="panel">
      <div className="panel-head">
        <span className="readout">
          {pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}
        </span>
        <button
          type="button"
          className="button"
          disabled={!pendingCount}
          onClick={() => void redaction.clearPending()}
        >
          Remove all
        </button>
      </div>
      <ul className="marks">
        {marks.map((mark) => (
          <li key={annotationKey(mark.ref)} className="mark">
            <button
              type="button"
              className="mark-go"
              onClick={() => stage.reveal(mark.page, { rect: mark.bounds })}
            >
              <span className="mark-page">Page {mark.pageIndex + 1}</span>
              <span className="mark-kind">{mark.kind === 'text' ? 'Text' : 'Area'}</span>
            </button>
            <button
              type="button"
              className="button"
              disabled={!redaction.canUnmark(mark.ref)}
              onClick={() => void redaction.unmark([mark.ref])}
            >
              Remove
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <MarkSome />
        <div className="viewer">
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <AnnotationLayer />
              </>
            )}
          </Stage>
          <PendingMarks />
        </div>
      </DocumentGate>
    </Viewer>
  );
}
```

**`review.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* The pages and the marks: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.viewer .stage {
  flex: 1 1 100%;
  box-sizing: border-box;
  height: 340px;
}

.panel {
  flex: 1 1 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  max-height: 260px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 1px 2px var(--shadow);
}

@container (min-width: 620px) {
  .viewer .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 420px;
  }

  .panel {
    flex: 0 0 250px;
    max-height: 420px;
  }
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 10px 10px 14px;
  border-bottom: 1px solid var(--line);
}

/* One mark: where it is, and a way to remove it */
.marks {
  flex: 1 1 auto;
  overflow-y: auto;
  margin: 0;
  padding: 6px;
  list-style: none;
}

.mark {
  display: flex;
  align-items: center;
  gap: 6px;
}

.mark-go {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 9px;
  padding: 8px 10px;
  cursor: pointer;
}

.mark-go:hover {
  background: var(--tint);
}

.mark-go:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

.mark-page {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.mark-kind {
  font-size: 13px;
  font-weight: 550;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Apply

```ts
const result = await redaction.applyAll(); // every mark
await redaction.apply([mark.ref]); // some marks
await redaction.applyPages([page.ref]); // the marks on some pages
```

This can't be undone. Ask first, and say what else goes with it: annotations a mark overlaps are
removed too, because they could show what was there. `estimateCollateral()` tells you how many:

```ts
const { count } = redaction.estimateCollateral();
if (!(await confirm(`Redact? ${count} annotations under the marks will be removed too.`))) return;
await redaction.applyAll();
```

After applying, download the document with `{ mode: 'rewrite' }`, so the removed content isn't left in
an earlier revision of the file. See [Saving & downloading](../documents/saving).

**`apply.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, saveFile, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import { redactionPlugin, useRedaction, useRedactionState } from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';

import './apply.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  redactionPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// The cover's title, in page coordinates: the shapes drawn over it go with it.
const TITLE = { x: 100, y: 212, width: 392, height: 156 };

function ApplyBar() {
  const redaction = useRedaction();
  const documents = useDocuments();
  const { pendingCount, applying, lastResult } = useRedactionState();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const [asking, setAsking] = useState(false);
  const started = useRef(false);

  // On load: the title marked.
  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void redaction.markArea(0, TITLE);
  }, [redaction, ready]);

  // The other annotations applying removes too: they could show what was there.
  const { count } = redaction.estimateCollateral();

  if (asking) {
    return (
      <div className="toolbar confirm" role="alertdialog" aria-label="Redact for good?">
        <span className="readout">
          Redact {pendingCount} {pendingCount === 1 ? 'mark' : 'marks'}?{' '}
          {count > 0 &&
            `${count} ${count === 1 ? 'annotation' : 'annotations'} under them go too. `}
          This can’t be undone.
        </span>
        <span className="spacer" />
        <button type="button" className="button" onClick={() => setAsking(false)}>
          Cancel
        </button>
        <button
          type="button"
          className="button danger"
          disabled={applying}
          onClick={() => void redaction.applyAll().finally(() => setAsking(false))}
        >
          Redact for good
        </button>
      </div>
    );
  }

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button danger"
        disabled={!pendingCount || !redaction.canApply()}
        onClick={() => setAsking(true)}
      >
        Redact…
      </button>
      <button
        type="button"
        className="button"
        disabled={!lastResult}
        title="A fresh file, without the earlier revision that still holds the content"
        onClick={() =>
          void documents
            .download(undefined, { mode: 'rewrite' })
            .then((bytes) => saveFile(bytes, 'redacted.pdf', 'application/pdf'))
        }
      >
        Download
      </button>
      <span className="spacer" />
      <output className="readout">
        {lastResult
          ? `Gone for good, with ${lastResult.removedAnnotationCount} other annotations`
          : `${pendingCount} ${pendingCount === 1 ? 'mark' : 'marks'} waiting`}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <ApplyBar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`apply.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --danger: #c4262e;
  --danger-strong: #a81f26;
  --danger-tint: #fdeced;
  --danger-line: #f3c4c7;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* The one button that can't be undone */
.button.danger {
  color: var(--surface);
  background: var(--danger);
  border-color: var(--danger);
}

.button.danger:hover {
  background: var(--danger-strong);
  border-color: var(--danger-strong);
}

/* Asking before what can't be undone */
.confirm {
  padding: 8px 8px 8px 14px;
  background: var(--danger-tint);
  border: 1px solid var(--danger-line);
  border-radius: 12px;
}

.confirm .readout {
  flex: 1 1 260px;
  font-weight: 550;
  line-height: 1.4;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## What shows in place of the content

By default a redacted area is black. Give it a label, such as "Classified", on a mark:

```ts
await redaction.updateLabel(mark.ref, { overlayText: 'Classified', repeat: true });
```

`repeat` fills the area with the label over and over; otherwise it shows once. The color and font of
every mark are settings.

**`label.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin, useAnnotationState } from '@embedpdf/react/annotation';
import {
  redactionPlugin,
  usePendingRedactions,
  useRedaction,
  useRedactionState,
} from '@embedpdf/react/redaction';
import { localEngine } from '@embedpdf/engine';

import './label.css';

const engine = localEngine();
// What applying paints over every mark: a dark blue area, its label in white.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  redactionPlugin({ overlay: { fill: '#1a2748', text: { color: '#ffffff' } } }),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// The cover's subtitle, in page coordinates.
const SUBTITLE = { x: 100, y: 378, width: 352, height: 114 };

function LabelBar() {
  const redaction = useRedaction();
  const stage = useStage();
  const { applying, lastResult } = useRedactionState();
  const [mark] = usePendingRedactions();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const [text, setText] = useState('Classified');
  const started = useRef(false);

  // On load: the subtitle marked, with a label that fills the area.
  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void redaction
      .markArea(0, SUBTITLE)
      .then(({ mark }) =>
        redaction.updateLabel(mark.ref, { overlayText: 'Classified', repeat: true }),
      )
      .then(() => stage.reveal(0, { rect: SUBTITLE }));
  }, [redaction, stage, ready]);

  return (
    <div className="toolbar">
      <form
        className="label-form"
        onSubmit={(event) => {
          event.preventDefault();
          if (mark) void redaction.updateLabel(mark.ref, { overlayText: text.trim() || null });
        }}
      >
        <input
          className="field"
          aria-label="Label"
          value={text}
          disabled={!mark}
          onChange={(event) => setText(event.target.value)}
        />
        <button type="submit" className="button" disabled={!mark}>
          Set label
        </button>
      </form>
      <label className="label">
        <input
          type="checkbox"
          checked={mark?.repeat ?? false}
          disabled={!mark}
          onChange={(event) =>
            mark && void redaction.updateLabel(mark.ref, { repeat: event.target.checked })
          }
        />
        Repeat
      </label>
      <button
        type="button"
        className="button danger"
        disabled={!mark || applying}
        onClick={() => void redaction.applyAll()}
      >
        Redact
      </button>
      <span className="spacer" />
      <output className="readout">
        {lastResult ? 'The label is part of the page now' : 'Redact to see the label'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <LabelBar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`label.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --danger: #c4262e;
  --danger-strong: #a81f26;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* A text box or a menu in the toolbar */
.field {
  appearance: none;
  min-width: 0;
  box-sizing: border-box;
  min-height: 34px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

select.field {
  appearance: auto;
  padding-right: 6px;
  cursor: pointer;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
}

/* The one button that can't be undone */
.button.danger {
  color: var(--surface);
  background: var(--danger);
  border-color: var(--danger);
}

.button.danger:hover {
  background: var(--danger-strong);
  border-color: var(--danger-strong);
}

/* The label and its button, together */
.label-form {
  display: flex;
  flex: 0 1 260px;
  gap: 8px;
  min-width: 0;
}

.label-form .field {
  flex: 1 1 auto;
  width: 100%;
}

.label input {
  accent-color: var(--accent);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Methods

Everything `useRedaction()` gives you.

| Method                                                             | What it does                                                                                                                                                                 |
| :----------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `markSelection()`                                                  | Marks the selected text, one mark per page, and clears the selection. Resolves `{ marks }`. [Mark from code](#mark-from-code)                                                |
| `markArea(page, rect)`, `markPage(page)`                           | Marks an area of a page, or the whole page. Resolves `{ mark }`.                                                                                                             |
| `markMatches(query, { pages? })`                                   | Marks every match of a [search](../text/search), or the ones on some pages. It replaces the current search. Resolves `{ marks }`.                                            |
| `unmark(refs)`                                                     | Removes marks without applying them. [Review the marks](#review-the-marks)                                                                                                   |
| `clearPending()`                                                   | Removes every mark                                                                                                                                                           |
| `updateLabel(ref, { overlayText, repeat? })`                       | Sets the label a mark shows once it's applied; `overlayText: null` clears it. Resolves `{ mark }`. [What shows in place of the content](#what-shows-in-place-of-the-content) |
| `listPending({ page? })`                                           | The marks not applied yet, in page order, or one page's. In a component, use `usePendingRedactions()`.                                                                       |
| `getPending(ref)`                                                  | One mark, or `null` when it isn't a mark waiting to be applied                                                                                                               |
| `estimateCollateral(refs?)`                                        | The other annotations applying would remove too, as `{ count, refs }`, for every mark or for the ones you pass. [Apply](#apply)                                              |
| `applyAll()`                                                       | Applies every mark in the document, on pages that haven't loaded too. Can't be undone. [Apply](#apply)                                                                       |
| `apply(refs)`, `applyPages(pages)`                                 | Applies some marks, or the marks on some pages. Can't be undone.                                                                                                             |
| `canMark()`, `canUnmark(ref)`, `canUpdateLabel(ref)`, `canApply()` | Whether marking, removing a mark, changing its label and applying are allowed. [Permissions](#permissions)                                                                   |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()`      | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useRedactionSettings()`. [Settings](#settings)                |

Applying resolves the result: each page's `status` (`'applied'` or `'unchanged'`) and how many
other annotations were removed. Applying is all or nothing: when a page fails, no page changes and
the promise rejects. Every method that returns a promise also
takes a `signal` in its options, to cancel it.

## State

Everything `useRedactionState()` gives you. Your component re-renders when a value changes. The method in the last column reads the same value once.

| Field          | What it is                                                            | Read once                                                    |
| :------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------- |
| `pendingCount` | How many marks are waiting to be applied                              | `getPendingCount()`, or `getPendingCount(page)` for one page |
| `applying`     | Whether marks are being applied right now                             | `isApplying()`                                               |
| `lastResult`   | The result of the last apply, from this session or another, or `null` | `getLastResult()`                                            |

The marks themselves come from `usePendingRedactions()`. Keeping them apart means a component that
only shows the count doesn't re-render for every mark.

## Settings

Set them when you register the plugin, and change them while the app runs with
`redaction.updateSettings({ … })`; `redaction.resetSettings()` goes back to what you registered, and
`useRedactionSettings()` gives you the current settings.

| Setting                   | What it does                                                                   | Default     |
| :------------------------ | :----------------------------------------------------------------------------- | :---------- |
| `overlay.fill`            | The color of a redacted area                                                   | `'#000000'` |
| `overlay.text.color`      | The color of a label                                                           | `'#ffffff'` |
| `overlay.text.fontFamily` | The label's font                                                               | Helvetica   |
| `overlay.text.fontSize`   | The label's size, or `0` to fit the area (a repeated label then draws at 12pt) | `0`         |

## Events

**`events.tsx`**

```tsx
import { useState } from 'react';
import { useRedactionEvent } from '@embedpdf/react/redaction';

export function RedactionNotice() {
  const [message, setMessage] = useState('');

  useRedactionEvent(
    (redaction) => redaction.onApplied,
    () => setMessage('Redacted: the content under the marks is gone'),
  );

  return <p aria-live="polite">{message}</p>;
}
```

| Event               | Fires when                                 | Carries               |
| :------------------ | :----------------------------------------- | :-------------------- |
| `onApplied`         | Marks were applied and the content is gone | `result`, `origin`    |
| `onPendingChanged`  | A mark was added, changed or removed       | `pages`               |
| `onSettingsChanged` | The settings changed                       | `settings`, `changed` |

## Permissions

| Permission           | What it allows                      | Check                           |
| :------------------- | :---------------------------------- | :------------------------------ |
| `annotations:create` | Marking                             | `redaction.canMark()`           |
| `annotations:delete` | Removing a mark before it's applied | `redaction.canUnmark(ref)`      |
| `annotations:update` | Changing a mark's label             | `redaction.canUpdateLabel(ref)` |
| `doc.redact`         | Applying                            | `redaction.canApply()`          |

A mark is an annotation, so removing it or changing its label follows the same rules as any
annotation, per mark. Applying also rewrites the pages and removes annotations, so `canApply()` needs
`doc.pages.modify` and `doc.annotate.modify` as well. A reviewer who may mark but not apply can
prepare redactions for someone else to approve.

## Good to know

### What is removed

The characters under a mark are removed, so they can't be selected, searched or copied; the pixels
of images inside it are erased; and the parts of lines, shapes and fills inside it are cut away. See
[Redaction](https://www.embedpdf.com/docs/engine/documents/redaction) for the details.

### Where the original is

The local engine redacts a copy of the file the browser already has, so the original was on the
user's device before it was redacted.

> **Redact before anyone receives the file**
>
> With CloudPDF, redaction happens on your server. People who may only see the redacted version never
> receive the original: not the file, not its text, not its images.
>
> [Redact with CloudPDF](https://www.cloudpdf.com/docs/headless/react/annotations/redaction)
