---
title: "Organizing pages — React"
description: "Rotate, reorder, delete, duplicate and insert pages, and extract pages into a new PDF. Edits are written into the document."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/documents/pages"
---

# Organizing pages

The page edit plugin changes the document's pages: rotate, reorder, delete, duplicate, and insert
new ones, blank or from another PDF. These are edits to the file, kept when it's downloaded. Register
`pageEditPlugin()` and use `usePageEdit()`:

**`organize.tsx`**

```tsx
import { useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput, PageRef } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';

import './organize.css';

const engine = localEngine();
const plugins = [renderPlugin(), pageEditPlugin()];

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()) };
};

const sameRef = (left: PageRef, right: PageRef) => left.objectNumber === right.objectNumber;

// Every page as a card. Click cards to select them; the toolbar edits every selected page at once.
function PageOrganizer() {
  const pageEdit = usePageEdit();
  const pages = usePageList();
  const canEdit = pageEdit.canEdit();
  // The second and third pages start selected. Refs, not indexes: they still name the
  // same pages after a reorder.
  const [selected, setSelected] = useState(() => pages.slice(1, 3).map((page) => page.ref));

  const isSelected = (page: PageRef) => selected.some((ref) => sameRef(ref, page));
  const toggle = (page: PageRef) =>
    setSelected(
      isSelected(page) ? selected.filter((ref) => !sameRef(ref, page)) : [...selected, page],
    );
  const count = selected.length;

  return (
    <>
      <div className="toolbar">
        <output className="readout">
          {count} of {pages.length} selected
        </output>
        <span className="spacer" />
        <button
          type="button"
          className="button"
          disabled={!canEdit || count === 0}
          onClick={() => pageEdit.rotateBy(selected, -90)}
        >
          ⟲ Rotate left
        </button>
        <button
          type="button"
          className="button"
          disabled={!canEdit || count === 0}
          onClick={() => pageEdit.rotateBy(selected, 90)}
        >
          ⟳ Rotate right
        </button>
        <button
          type="button"
          className="button"
          disabled={!canEdit || count === 0}
          onClick={() => pageEdit.reorder(selected, 'start')}
        >
          Move to front
        </button>
        <button
          type="button"
          className="button danger"
          // A document keeps at least one page.
          disabled={!canEdit || count === 0 || count === pages.length}
          onClick={async () => {
            await pageEdit.delete(selected);
            setSelected([]);
          }}
        >
          Delete
        </button>
      </div>
      <ol className="pages">
        {pages.map((page) => (
          <li key={page.ref.objectNumber}>
            <button
              type="button"
              className="card"
              aria-pressed={isSelected(page.ref)}
              onClick={() => toggle(page.ref)}
            >
              <PageView page={page.ref} width={120} className="thumbnail">
                <RenderLayer />
              </PageView>
              <span className="label">
                Page {page.index + 1}
                {page.rotation !== 0 && <span className="turn"> · {page.rotation}°</span>}
              </span>
            </button>
          </li>
        ))}
      </ol>
    </>
  );
}

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

**`organize.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.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;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

.button.danger {
  color: var(--danger);
}

/* The pages in one row, scrolling sideways when they don't fit */
.pages {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  overflow-x: auto;
  margin: 0;
  padding: 16px;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

/* A card is as wide as its page: a page turned a quarter makes it wider */
.pages > li {
  flex: none;
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 140px;
  font: inherit;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

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

.card[aria-pressed='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

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

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.turn {
  font-weight: 500;
  color: var(--muted);
}

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

```ts
await pageEdit.rotateBy([page.ref], 90);
await pageEdit.reorder([page.ref], 'start'); // to the front
await pageEdit.delete([page.ref]);
```

After an edit, everything that shows pages updates by itself: the Stage, thumbnails, page numbers. On
a shared document, the others see the change too.

Calls that act on pages take a list of them, so one call works for a single page and for a
multi-selection.

## Rotate pages

```ts
await pageEdit.rotateBy([page.ref], 90); // a quarter turn clockwise from where it is
await pageEdit.rotateBy([page.ref], -90); // and back
await pageEdit.setRotation([pageA.ref, pageB.ref], 180); // the same rotation for several
```

**`rotate.tsx`**

```tsx
import { useEffect } from 'react';
import { Viewer, DocumentGate, usePageList } 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 { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';

import './rotate.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), pageEditPlugin()];

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()) };
};

// Rotates the page you're on, in the file: the turn is kept when the document is downloaded.
function RotateToolbar() {
  const pageEdit = usePageEdit();
  const pages = usePageList();
  const currentPageIndex = useStageState((state) => state.currentPageIndex);
  const page = pages[currentPageIndex];

  // The first page starts a quarter turn clockwise. setRotation() sets the same rotation
  // however often it runs; rotateBy() would turn it again.
  useEffect(() => {
    void pageEdit.setRotation([0], 90);
  }, [pageEdit]);

  if (!page) return null;
  const canEdit = pageEdit.canEdit();
  return (
    <div className="toolbar">
      <output className="readout">
        Page {currentPageIndex + 1} · {page.rotation}°
      </output>
      <span className="spacer" />
      <button
        type="button"
        className="button"
        disabled={!canEdit}
        onClick={() => pageEdit.rotateBy([page.ref], -90)}
      >
        ⟲ Rotate left
      </button>
      <button
        type="button"
        className="button"
        disabled={!canEdit}
        onClick={() => pageEdit.rotateBy([page.ref], 90)}
      >
        ⟳ Rotate right
      </button>
      <button
        type="button"
        className="button"
        disabled={!canEdit}
        onClick={() =>
          pageEdit.setRotation(
            pages.map((each) => each.ref),
            0,
          )
        }
      >
        Every page upright
      </button>
    </div>
  );
}

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

**`rotate.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.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;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

/* 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);
}
```

This rotates the page in the file. To only turn how pages are shown, use the Stage's
[view rotation](../viewing/stage#rotate-the-view). Keep the two on different buttons.

## Reorder pages

`reorder()` takes the pages and where they go:

```ts
await pageEdit.reorder([page.ref], 'start'); // to the front
await pageEdit.reorder([page.ref], 'end'); // to the back
await pageEdit.reorder([pageA.ref, pageB.ref], { after: pageC.ref }); // after another page
await pageEdit.reorder([page.ref], { before: pageC.ref });
```

Several pages keep their order and land together.

**`reorder.tsx`**

```tsx
import { useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';

import './reorder.css';

const engine = localEngine();
const plugins = [renderPlugin(), pageEditPlugin()];

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()) };
};

// Pick a page, then move it. Each button uses another placement.
function PageOrder() {
  const pageEdit = usePageEdit();
  const pages = usePageList();
  // The last page starts selected. Its ref names it wherever it moves.
  const [selected, setSelected] = useState(() => pages[pages.length - 1]?.ref ?? null);

  const page = pages.find((each) => each.ref.objectNumber === selected?.objectNumber);
  const before = page && pages[page.index - 1];
  const after = page && pages[page.index + 1];
  const canEdit = pageEdit.canEdit();

  return (
    <>
      <div className="toolbar">
        <output className="readout">
          {page ? `Page ${page.index + 1} selected` : 'Pick a page'}
        </output>
        <span className="spacer" />
        <button
          type="button"
          className="button"
          disabled={!canEdit || !before}
          onClick={() => page && pageEdit.reorder([page.ref], 'start')}
        >
          ⇤ To the front
        </button>
        <button
          type="button"
          className="button"
          disabled={!canEdit || !before}
          onClick={() => page && before && pageEdit.reorder([page.ref], { before: before.ref })}
        >
          ← Earlier
        </button>
        <button
          type="button"
          className="button"
          disabled={!canEdit || !after}
          onClick={() => page && after && pageEdit.reorder([page.ref], { after: after.ref })}
        >
          Later →
        </button>
        <button
          type="button"
          className="button"
          disabled={!canEdit || !after}
          onClick={() => page && pageEdit.reorder([page.ref], 'end')}
        >
          To the back ⇥
        </button>
      </div>
      <ol className="strip">
        {pages.map((each) => (
          <li key={each.ref.objectNumber}>
            <button
              type="button"
              className="card"
              aria-pressed={each.ref.objectNumber === selected?.objectNumber}
              onClick={() => setSelected(each.ref)}
            >
              <PageView page={each.ref} width={110} className="thumbnail">
                <RenderLayer />
              </PageView>
              <span className="label">{each.index + 1}</span>
            </button>
          </li>
        ))}
      </ol>
    </>
  );
}

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

**`reorder.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.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;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

/* The pages in their order: a row that scrolls sideways where it doesn't fit */
.strip {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  margin: 0;
  padding: 16px;
  overflow-x: auto;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  font: inherit;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

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

.card[aria-pressed='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

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

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

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

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

## Delete pages

```ts
await pageEdit.delete([pageA.ref, pageB.ref]);
```

A document always keeps at least one page: deleting every page is refused.

## Insert pages

```ts
// A blank page after this one, the size of an A4 sheet
await pageEdit.insertBlank({ placement: { after: page.ref }, size: { width: 595, height: 842 } });

// Pages from another PDF: all of them, or the ones you name
await pageEdit.insertFromBytes(otherPdf, { placement: 'end' });
await pageEdit.insertFromBytes(otherPdf, { pageIndexes: [0, 2], placement: 'start' });

// Pages from another document open in the viewer
await pageEdit.insertFromDocument(otherDocumentId, [pageRef], { placement: 'end' });

// Copies of pages, right after them
await pageEdit.duplicate([page.ref]);
```

Each resolves with `{ pages }`: the new pages, so you can scroll to them or select them. Without a
`size`, a blank page takes the size of the page it's placed next to.

**`insert.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput, PageRef } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';

import './insert.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), pageEditPlugin()];

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()) };
};

/** Another PDF's bytes, to insert pages from. */
const otherPdf = async () => (await fetch('https://snippet.embedpdf.com/ebook.pdf')).arrayBuffer();

// Inserts next to the page you're on, then goes to the first new page.
function InsertToolbar() {
  const pageEdit = usePageEdit();
  const stage = useStage();
  const pages = usePageList();
  const currentPageIndex = useStageState((state) => state.currentPageIndex);
  const page = pages[currentPageIndex];
  const [added, setAdded] = useState<readonly PageRef[]>([]);

  // Every insert resolves { pages }: the new pages, to go to or select.
  const show = (result: { pages: readonly PageRef[] }) => {
    setAdded(result.pages);
    stage.goToPage(result.pages[0]);
  };

  // A blank page after the cover, once, on load.
  const inserted = useRef(false);
  useEffect(() => {
    if (inserted.current || !pages[0]) return;
    inserted.current = true;
    void pageEdit.insertBlank({ placement: { after: pages[0].ref } }).then(show);
  });

  if (!page) return null;
  const canEdit = pageEdit.canEdit();
  const positions = added
    .map((ref) => pages.findIndex((each) => each.ref.objectNumber === ref.objectNumber) + 1)
    .filter((position) => position > 0);

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={!canEdit}
        onClick={() => pageEdit.insertBlank({ placement: { after: page.ref } }).then(show)}
      >
        + Blank page after
      </button>
      <button
        type="button"
        className="button"
        disabled={!canEdit || !pageEdit.canExtract()}
        onClick={() => pageEdit.duplicate([page.ref]).then(show)}
      >
        Duplicate page
      </button>
      <button
        type="button"
        className="button"
        disabled={!canEdit}
        onClick={async () =>
          show(
            await pageEdit.insertFromBytes(await otherPdf(), {
              pageIndexes: [0, 2],
              placement: { after: page.ref },
            }),
          )
        }
      >
        + Pages 1 and 3 of another PDF
      </button>
      <span className="spacer" />
      <output className="readout">
        {positions.length > 0
          ? `New: page ${positions.join(' and ')} of ${pages.length}`
          : `${pages.length} pages`}
      </output>
    </div>
  );
}

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

**`insert.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.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;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

/* 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);
}
```

## Extract pages

`extract()` gives you some pages as a new PDF, without changing this document:

```ts
const bytes = await pageEdit.extract([pageA.ref, pageB.ref]);
saveFile(bytes, 'chapter-2.pdf');
```

**`extract.tsx`**

```tsx
import { useState } from 'react';
import { Viewer, DocumentGate, saveFile, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput, PageRef } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';

import './extract.css';

const engine = localEngine();
const plugins = [renderPlugin(), pageEditPlugin()];

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()) };
};

// Tick pages, then download them as a PDF of their own. This document doesn't change.
function ExtractPages() {
  const pageEdit = usePageEdit();
  const pages = usePageList();
  // The middle two pages start ticked.
  const [ticked, setTicked] = useState(() => pages.slice(1, 3).map((page) => page.ref));
  const [saved, setSaved] = useState<string | null>(null);

  const isTicked = (ref: PageRef) => ticked.some((each) => each.objectNumber === ref.objectNumber);
  const toggle = (ref: PageRef) =>
    setTicked(
      isTicked(ref)
        ? ticked.filter((each) => each.objectNumber !== ref.objectNumber)
        : [...ticked, ref],
    );

  // In document order, whatever order they were ticked in.
  const chosen = pages.filter((page) => isTicked(page.ref)).map((page) => page.ref);

  const download = async () => {
    const bytes = await pageEdit.extract(chosen);
    saveFile(bytes, 'pages.pdf');
    setSaved(`pages.pdf · ${chosen.length} pages · ${Math.round(bytes.byteLength / 1024)} KB`);
  };

  return (
    <>
      <div className="toolbar">
        <button
          type="button"
          className="button primary"
          disabled={chosen.length === 0 || !pageEdit.canExtract()}
          onClick={download}
        >
          Download {chosen.length === 1 ? '1 page' : `${chosen.length} pages`} as a PDF
        </button>
        <output className="readout">{saved}</output>
      </div>
      <ol className="grid">
        {pages.map((page) => (
          <li key={page.ref.objectNumber}>
            <label className="card" data-ticked={isTicked(page.ref)}>
              <PageView page={page.ref} width={110} className="thumbnail">
                <RenderLayer />
              </PageView>
              <span className="label">
                <input
                  type="checkbox"
                  checked={isTicked(page.ref)}
                  onChange={() => toggle(page.ref)}
                />
                Page {page.index + 1}
              </span>
            </label>
          </li>
        ))}
      </ol>
    </>
  );
}

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

**`extract.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

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

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

.button.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

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

/* The pages: as many in a row as fit */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  align-items: end;
  gap: 12px;
  margin: 0;
  padding: 16px;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

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

.card[data-ticked='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

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

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

## Where a page goes

| Placement             | Means                  |
| :-------------------- | :--------------------- |
| `{ after: pageRef }`  | Right after that page  |
| `{ before: pageRef }` | Right before that page |
| `'start'`             | Before the first page  |
| `'end'`               | After the last page    |

A placement names a page, not a position, so it stays right when pages move while the edit waits
its turn. The page can also be an index, as everywhere in this plugin: the page at that index
when you call.

## Methods

Everything `usePageEdit()` gives you.

| Method                                                  | What it does                                                                                                                    |
| :------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------ |
| `rotateBy(pages, delta)`                                | Turns each page by `90`, `-90` or `180` degrees from where it is. [Rotate pages](#rotate-pages)                                 |
| `setRotation(pages, rotation)`                          | Gives every page the same rotation: `0`, `90`, `180` or `270`                                                                   |
| `reorder(pages, placement)`                             | Moves pages to a new place. Several pages keep their order and land together. [Reorder pages](#reorder-pages)                   |
| `delete(pages)`                                         | Deletes pages. Deleting every page is refused. [Delete pages](#delete-pages)                                                    |
| `insertBlank({ placement?, size?, count? })`            | Inserts blank pages, one unless you pass a `count`. Resolves `{ pages }`, the new pages. [Insert pages](#insert-pages)          |
| `insertFromBytes(bytes, { pageIndexes?, placement? })`  | Inserts the pages of another PDF, or only the ones at `pageIndexes`. Resolves `{ pages }`.                                      |
| `insertFromDocument(documentId, pages, { placement? })` | Inserts copies of pages from another document open in the viewer. Resolves `{ pages }`.                                         |
| `duplicate(pages, { placement? })`                      | Copies pages, right after the last of them unless you pass a placement. Resolves `{ pages }`.                                   |
| `extract(pages)`                                        | Copies pages into a new PDF and resolves its bytes. This document doesn't change. [Extract pages](#extract-pages)               |
| `canEdit()`, `canExtract()`                             | Whether rotating, reordering, deleting and inserting are allowed, and whether copying pages out is. [Permissions](#permissions) |

`placement` is one of the values in [Where a page goes](#where-a-page-goes); without one, new pages go
at the end. Every method that returns a promise also takes a `signal` in its options, to cancel it.

## Events

Page changes are document events, so every part of your app hears them, whichever plugin made the
change:

**`page-count.tsx`**

```tsx
import { useState } from 'react';
import { useDocumentsEvent } from '@embedpdf/react/runtime';

export function PageCount() {
  const [pageCount, setPageCount] = useState(0);

  useDocumentsEvent(
    (documents) => documents.onPagesChanged,
    ({ pages }) => setPageCount(pages.length),
  );

  return <span>{pageCount} pages</span>;
}
```

The page edit plugin has no events of its own. `onPagesChanged` and what it carries are listed under
[Opening documents](opening#events).

## Permissions

| Permission           | What it allows                                                              | Check                   |
| :------------------- | :-------------------------------------------------------------------------- | :---------------------- |
| `doc.pages.assemble` | Rotate, reorder, delete and insert pages                                    | `pageEdit.canEdit()`    |
| `doc.download`       | `extract()`, `duplicate()` and `insertFromDocument()`, which copy pages out | `pageEdit.canExtract()` |

## Good to know

### Pages have a ref, not just a number

Each page has a `ref`: its identity in the file. A page's index changes the moment a page before it
moves or goes; its ref never does. So a call still hits the page the user clicked, even if the
document changed in between. You get refs from `usePageList()` (`page.ref`), and anything that
points at a page carries one: a search result's `page`, an annotation's `page`.

### What moves with a page

Annotations, links and form fields belong to their page, so they move, rotate and go with it.
Rotation leaves annotations where they are on the page; they turn with it.

> Page edits are saved like every other change; see [Saving & downloading](saving).
