---
title: "Several documents — React"
description: "Open several documents at once: tabs, side-by-side panes, and which document each part of your UI talks to."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/documents/tabs"
---

# Several documents

A viewer can hold several documents at once, like tabs in a browser. One of them is the active
document, and every component talks to it unless you say otherwise:

**`tabs.tsx`**

```tsx
import { useDocuments, useDocumentsState } from '@embedpdf/react/runtime';

export function Tabs() {
  const documents = useDocuments();
  const { documents: open, activeId } = useDocumentsState();

  return (
    <div role="tablist">
      {open.map((document) => (
        <div key={document.id}>
          <button
            role="tab"
            aria-selected={document.id === activeId}
            onClick={() => documents.setActive(document.id)}
          >
            {document.name}
          </button>
          <button aria-label={`Close ${document.name}`} onClick={() => documents.close(document.id)}>
            ×
          </button>
        </div>
      ))}
    </div>
  );
}
```

Each document keeps its own state: its zoom and scroll position, its selection, its search and its
active tool. Switch tabs and back, and everything is where you left it.

**`tabs.tsx`**

```tsx
import {
  Viewer,
  DocumentGate,
  useDocument,
  useDocuments,
  useDocumentsState,
} from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './tabs.css';

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

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

function Tabs() {
  const documents = useDocuments();
  const { documents: open, activeId } = useDocumentsState();

  return (
    <div className="tabs" role="tablist">
      {open.map((document) => (
        <div key={document.id} className="tab" data-active={document.id === activeId}>
          <button
            type="button"
            role="tab"
            className="name"
            aria-selected={document.id === activeId}
            onClick={() => documents.setActive(document.id)}
          >
            {document.name}
          </button>
          <button
            type="button"
            className="close"
            aria-label={`Close ${document.name}`}
            onClick={() => documents.close(document.id)}
          >
            ×
          </button>
        </div>
      ))}
      <button
        type="button"
        className="button"
        aria-label="Open another copy"
        onClick={() => documents.open(ebook, { name: `Copy ${open.length + 1}` })}
      >
        +
      </button>
    </div>
  );
}

// The active tab: rename it, move it to the front, and zoom it. Each tab keeps its own zoom.
function TabTools() {
  const documents = useDocuments();
  const { id, name } = useDocument();
  const stage = useStage();
  const zoomLevel = useStageState((state) => state.zoomLevel);

  return (
    <div className="toolbar">
      <input
        className="field"
        aria-label="Tab name"
        value={name ?? ''}
        onChange={(event) => documents.rename(id, event.target.value)}
      />
      <button type="button" className="button" onClick={() => documents.move(id, 0)}>
        Move to front
      </button>
      <button
        type="button"
        className="button"
        aria-label="Zoom out"
        onClick={() => stage.zoomOut()}
      >
        −
      </button>
      <button type="button" className="button" aria-label="Zoom in" onClick={() => stage.zoomIn()}>
        +
      </button>
      <output className="readout">{Math.round(zoomLevel * 100)}%</output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[
        { source: ebook, name: 'Contract' },
        { source: ebook, name: 'Report' },
      ]}
    >
      <Tabs />
      <DocumentGate fallback={<p className="loading">Opening…</p>}>
        <TabTools />
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`tabs.css`**

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

/* The tab strip: one tab per document, then the button that opens another */
.tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
}

.tab {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  box-shadow: 0 1px 2px var(--shadow);
}

.tab[data-active='true'] {
  background: var(--tint);
  border-color: var(--focus);
}

.tab button {
  font: inherit;
  font-size: 13px;
  color: var(--muted);
  background: none;
  border: 0;
  padding: 7px 10px;
  cursor: pointer;
}

.tab[data-active='true'] .name {
  color: var(--ink);
  font-weight: 600;
}

.tab .close {
  padding-left: 0;
  font-size: 15px;
  line-height: 1;
}

.tab button:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 9px;
}

/* What you do to the active tab */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

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

.field {
  appearance: none;
  flex: 0 1 180px;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 10px;
}

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

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

/* The pages */
.stage {
  height: 400px;
  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);
}
```

## Manage the tabs

```ts
documents.setActive(id);
documents.rename(id, 'Signed contract');
documents.move(id, 0); // to the front
await documents.close(id);
```

Each entry in `documents`, from `useDocumentsState()`, has the document's `id`, its `name`, and its
`status`: `'loading'`, `'locked'`, `'ready'` or `'error'`, so a tab can show a spinner or a lock.
Every field is listed under [State](#state).

## Talk to a specific document

A component talks to the active document. Inside `<DocumentScope>`, it talks to the one you name,
whatever is active:

**`document-scope.tsx`**

```tsx
import { DocumentScope } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { ZoomControls } from './ZoomControls';

export function DocumentView({ documentId }: { documentId: string }) {
  return (
    <DocumentScope id={documentId}>
      <Stage>{() => <RenderLayer />}</Stage>
      <ZoomControls />
    </DocumentScope>
  );
}
```

Everything inside, such as the Stage, `useSearch()` and `useSelection()`, uses that document.
That's how you show two documents at once, or a preview of one while another is active.

**`scope.tsx`**

```tsx
import {
  Viewer,
  DocumentGate,
  DocumentScope,
  useDocument,
  useDocumentsState,
} from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './scope.css';

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

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

// The same component in both panes: each talks to the document its <DocumentScope> names.
function Header() {
  const { name, pageCount } = useDocument();
  const stage = useStage();
  const zoomLevel = useStageState((state) => state.zoomLevel);

  return (
    <div className="header">
      <strong>{name}</strong>
      {pageCount} pages
      <button
        type="button"
        className="button"
        aria-label="Zoom out"
        onClick={() => stage.zoomOut()}
      >
        −
      </button>
      <output className="readout">{Math.round(zoomLevel * 100)}%</output>
      <button type="button" className="button" aria-label="Zoom in" onClick={() => stage.zoomIn()}>
        +
      </button>
    </div>
  );
}

function SideBySide() {
  const { documents } = useDocumentsState();
  return (
    <div className="split">
      {documents.map((document) => (
        <section key={document.id} className="pane">
          <DocumentScope id={document.id}>
            <DocumentGate fallback={<p className="loading">Opening…</p>}>
              <Header />
              <Stage className="stage">{() => <RenderLayer />}</Stage>
            </DocumentGate>
          </DocumentScope>
        </section>
      ))}
    </div>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[
        { source: ebook, name: 'Original' },
        { source: ebook, name: 'Revised' },
      ]}
    >
      <SideBySide />
    </Viewer>
  );
}
```

**`scope.css`**

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

/* Two documents: stacked, and side by side where there's room */
.split {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.pane {
  flex: 1 1 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

@container (min-width: 620px) {
  .pane {
    flex: 1 1 0;
  }
}

/* A pane's header: its document, and its own zoom */
.header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--muted);
}

.header strong {
  margin-right: auto;
  color: var(--ink);
  font-weight: 600;
}

.button {
  min-width: 34px;
  font: inherit;
  font-size: 14px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 8px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

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

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

.readout {
  min-width: 44px;
  text-align: center;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* The pages */
.stage {
  height: 340px;
  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);
}
```

## Side by side

The view manager arranges documents into panes, each with its own tabs, like split editors in a
code editor. Register `viewManagerPlugin()`, then render one Stage per pane:

**`panes.tsx`**

```tsx
import { DocumentScope } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { useViewManager, useViewManagerState } from '@embedpdf/react/view-manager';
import { PaneTabs } from './PaneTabs';

export function Panes() {
  const views = useViewManager();
  const { panes, focusedPaneId } = useViewManagerState();

  return (
    <div className="panes">
      {panes.map((pane) => (
        <section key={pane.id} data-focused={pane.id === focusedPaneId} onFocus={() => views.setFocusedPane(pane.id)}>
          <PaneTabs pane={pane} />
          {pane.activeDocumentId && (
            <DocumentScope id={pane.activeDocumentId}>
              <Stage>{() => <RenderLayer />}</Stage>
            </DocumentScope>
          )}
        </section>
      ))}
    </div>
  );
}
```

```ts
views.splitPane(documentId); // the document in a new pane, beside the focused one
views.setActiveDocument(paneId, documentId); // show another of the pane's tabs
views.moveDocumentBetween(fromPaneId, toPaneId, documentId); // drag a tab to another pane
views.removePane(paneId); // its documents move to another pane
```

A document opened with `documents.open()` lands in the focused pane. Every call is listed under
[Methods](#methods).

**`panes.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate, DocumentScope, useDocumentsState } 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 {
  useViewManager,
  useViewManagerState,
  viewManagerPlugin,
  type PaneInfo,
} from '@embedpdf/react/view-manager';
import { localEngine } from '@embedpdf/engine';

import './panes.css';

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

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

function Pane({ pane, canRemove }: { pane: PaneInfo; canRemove: boolean }) {
  const views = useViewManager();
  const focusedPaneId = useViewManagerState((state) => state.focusedPaneId);
  const { documents } = useDocumentsState();
  const nameOf = (id: string) => documents.find((document) => document.id === id)?.name ?? id;

  return (
    <section
      className="pane"
      data-focused={pane.id === focusedPaneId}
      onPointerDown={() => views.setFocusedPane(pane.id)}
    >
      <div className="bar" role="tablist">
        {pane.documentIds.map((id) => (
          <button
            key={id}
            type="button"
            role="tab"
            className="tab"
            aria-selected={id === pane.activeDocumentId}
            onClick={() => views.setActiveDocument(pane.id, id)}
          >
            {nameOf(id)}
          </button>
        ))}
        <button
          type="button"
          className="button"
          disabled={!pane.activeDocumentId || pane.documentIds.length < 2}
          onClick={() => views.splitPane(pane.activeDocumentId!, { from: pane.id })}
        >
          Split
        </button>
        <button
          type="button"
          className="button"
          disabled={!canRemove}
          onClick={() => views.removePane(pane.id)}
        >
          Close pane
        </button>
      </div>
      {pane.activeDocumentId ? (
        <DocumentScope id={pane.activeDocumentId}>
          <DocumentGate fallback={<p className="empty">Opening…</p>}>
            <Stage className="stage">{() => <RenderLayer />}</Stage>
          </DocumentGate>
        </DocumentScope>
      ) : (
        <p className="empty">No document in this pane.</p>
      )}
    </section>
  );
}

function Panes() {
  const views = useViewManager();
  const { panes } = useViewManagerState();

  // On load, both documents land in the first pane: put the second one beside it, once.
  const splitOnLoad = useRef(true);
  useEffect(() => {
    if (splitOnLoad.current && panes.length === 1 && panes[0].documentIds.length === 2) {
      splitOnLoad.current = false;
      views.splitPane(panes[0].documentIds[1]);
    }
  }, [panes, views]);

  return (
    <div className="panes">
      {panes.map((pane) => (
        <Pane key={pane.id} pane={pane} canRemove={panes.length > 1} />
      ))}
    </div>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[
        { source: ebook, name: 'Contract' },
        { source: ebook, name: 'Report' },
      ]}
    >
      <Panes />
    </Viewer>
  );
}
```

**`panes.css`**

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

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

.pane {
  flex: 1 1 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
}

/* The focused pane is where documents you open land */
.pane[data-focused='true'] {
  border-color: var(--focus);
}

@container (min-width: 620px) {
  .pane {
    flex: 1 1 0;
  }
}

/* A pane's tabs, then what you do to the pane */
.bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.tab {
  font: inherit;
  font-size: 13px;
  color: var(--muted);
  background: none;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 10px;
  cursor: pointer;
}

.tab[aria-selected='true'] {
  color: var(--ink);
  font-weight: 600;
  background: var(--tint);
  border-color: var(--line-strong);
}

.button {
  font: inherit;
  font-size: 12px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 10px;
  cursor: pointer;
}

.button:first-of-type {
  margin-left: auto;
}

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

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

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

/* The pages */
.stage {
  height: 320px;
  border-radius: 10px;
  background: var(--canvas);
}

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

## Methods

What `useDocuments()` gives you for several documents. Opening and closing are on
[Opening documents](opening#methods), downloading and printing on
[Saving & downloading](saving#methods).

| Method              | What it does                                                                                                                     |
| :------------------ | :------------------------------------------------------------------------------------------------------------------------------- |
| `setActive(id)`     | Makes a document the active one: the one every component talks to outside `<DocumentScope>`. [Manage the tabs](#manage-the-tabs) |
| `rename(id, name)`  | Changes the name its tab shows. The file doesn't change.                                                                         |
| `move(id, index)`   | Moves a document to another place in the tab order                                                                               |
| `swap(id, otherId)` | Swaps two documents in the tab order                                                                                             |
| `setOrder(ids)`     | Puts every document in a new order at once. `ids` holds the id of every open document.                                           |

Everything `useViewManager()` gives you.

| Method                                                          | What it does                                                                                                                                              |
| :-------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `splitPane(documentId, { from? })`                              | Moves an open document into a new pane beside the focused one, or beside `from`, and focuses it. Returns the new pane's id. [Side by side](#side-by-side) |
| `createPane({ documentIds? })`                                  | Adds a pane, empty or with these documents moved into it, and focuses it. Returns its id.                                                                 |
| `removePane(paneId, { moveDocumentsTo? })`                      | Closes a pane. Its documents move to `moveDocumentsTo`, or else to the focused or the last pane.                                                          |
| `movePane(paneId, index)`                                       | Moves a pane to another place in the order, for reordering panes by dragging                                                                              |
| `setFocusedPane(paneId)`                                        | Makes a pane the focused one, where new documents open. `null` focuses none.                                                                              |
| `setActiveDocument(paneId, documentId)`                         | Shows another of the pane's documents                                                                                                                     |
| `addDocument(paneId, documentId, index?)`                       | Adds a document to a pane's tabs. A document is in one pane at a time, so it leaves the pane it was in.                                                   |
| `removeDocument(paneId, documentId)`                            | Takes a document out of a pane, without closing it                                                                                                        |
| `moveDocumentWithin(paneId, documentId, index)`                 | Moves a tab to another place inside its pane                                                                                                              |
| `moveDocumentBetween(fromPaneId, toPaneId, documentId, index?)` | Moves a tab from one pane to another, for dragging tabs between panes                                                                                     |

## State

Everything `useDocumentsState()` 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                                        |
| :---------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
| `documents` | Every open document, in tab order, including ones that are loading, locked or failed. Each has the fields [`useDocument()`](opening#state) gives you. | `list()`, `has(id)`, `getCount()`, `getOrder()`  |
| `activeId`  | The active document's id, or `null` with no document open                                                                                             | `getActiveId()`, or `getActive()` for its fields |

`has(id)` says whether a document is open, `getCount()` how many are, and `getOrder()` gives just their
ids. One document's fields come from `get(id)`, on [Opening documents](opening#state).

Everything `useViewManagerState()` 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                                                                           |
| :-------------- | :------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------- |
| `panes`         | Every pane, in order. Each has its `id`, its `documentIds` (its tabs, in order) and the `activeDocumentId` it shows. | `listPanes()`, `getPane(paneId)`, `getPaneOrder()`, `getPaneOfDocument(documentId)` |
| `focusedPaneId` | The pane new documents open in, or `null`                                                                            | `getFocusedPaneId()`                                                                |

`getPane(paneId)` reads one pane, `getPaneOrder()` gives just their ids, and
`getPaneOfDocument(documentId)` says which pane holds a document.

## Events

**`active-route.tsx`**

```tsx
import { useRouter } from 'next/navigation';
import { useDocumentsEvent } from '@embedpdf/react/runtime';

export function DocumentRoute() {
  const router = useRouter();

  useDocumentsEvent(
    (documents) => documents.onActiveChanged,
    ({ documentId }) => router.replace(`/documents/${documentId}`),
  );

  return null;
}
```

| Event                          | Fires when                             | Carries                                |
| :----------------------------- | :------------------------------------- | :------------------------------------- |
| `onActiveChanged`              | Another document became the active one | `documentId`, `previousDocumentId`     |
| View manager `onPaneCreated`   | A pane was added                       | `paneId`                               |
| View manager `onPaneRemoved`   | A pane was closed                      | `paneId`                               |
| View manager `onFocusChanged`  | Another pane got the focus             | `paneId`, or `null` when none has it   |
| View manager `onDocumentMoved` | A document moved between panes         | `documentId`, `fromPaneId`, `toPaneId` |

The view manager's events come from `useViewManagerEvent()`. Opening and closing documents are on
[Opening documents](opening#events).

## Good to know

### Memory

Each open document holds its file in the engine's memory. Close documents the user is done with,
especially large ones.

### One Stage per view

A Stage shows one document. For two documents at once, render two Stages, each in its own
`<DocumentScope>`. For two views of one document, see
[Two views of one document](../viewing/stage#two-views-of-one-document).
