EmbedPDF

Selecting & copying

The selection plugin lets people select text with the mouse or a finger, and lets your code select the same text. Register interactionPlugin() and selectionPlugin(), and put <SelectionLayer> above the page:

Loading live preview…

import { useEffect } from 'react';
import { Viewer, DocumentGate, usePageList } 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 { SelectionLayer, selectionPlugin, useSelection } from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './basic.css';

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

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

// Something to see on load: the title on the cover, selected from code.
function SelectTitle() {
  const selection = useSelection();
  const cover = usePageList()[0]?.ref;

  useEffect(() => {
    if (cover) selection.select({ page: cover, start: 10, count: 52 });
  }, [selection, cover]);

  return null;
}

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

Try it: the cover’s title starts out selected from code. Drag over text, double-click a word, or triple-click a line. A selection can run from one page onto the next. On a touch screen, long-press a word and drag the handles.

interactionPlugin() is the input system every tool shares: the Stage passes the pointer to it, the selection plugin handles it, and <SelectionLayer> draws the result. So you can restyle or replace the highlight without touching how selecting works.

Selecting uses where the characters are on the page, not the text itself. Nothing reads the page’s text until your code asks for it, and a document can allow selecting while it forbids copying (see Permissions).

Select text from code#

useSelection() does from code what the pointer does, and the highlight follows:

Loading live preview…

import { useEffect } from 'react';
import { Viewer, DocumentGate, usePageList } 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 {
  SelectionLayer,
  selectionPlugin,
  useSelection,
  useSelectionState,
} from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './programmatic.css';

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

// On the cover: the characters of its title, and a point on its word "Viewers", in page coordinates.
const TITLE = { start: 10, count: 52 };
const POINT = { x: 260, y: 242 };

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

// Every button selects on the cover, the first page: by its ref, or by its index, 0.
function SelectionToolbar() {
  const selection = useSelection();
  const { hasSelection, range, pages } = useSelectionState();
  const cover = usePageList()[0]?.ref;

  // The title is selected on load.
  useEffect(() => {
    if (cover) selection.select({ page: cover, ...TITLE });
  }, [selection, cover]);

  const canSelect = selection.canSelect();
  let summary = 'Nothing selected';
  if (pages.length > 1) summary = `On ${pages.length} pages`;
  else if (range) summary = `${range.end.index - range.start.index} characters`;

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={!canSelect || !cover}
        onClick={() => cover && selection.select({ page: cover, ...TITLE })}
      >
        Title
      </button>
      <button
        type="button"
        className="button"
        disabled={!canSelect}
        onClick={() => selection.selectWordAt(0, POINT)}
      >
        Word
      </button>
      <button
        type="button"
        className="button"
        disabled={!canSelect}
        onClick={() => selection.selectLineAt(0, POINT)}
      >
        Line
      </button>
      <button
        type="button"
        className="button"
        disabled={!canSelect}
        onClick={() => selection.selectPage(0)}
      >
        Page
      </button>
      <button
        type="button"
        className="button"
        disabled={!canSelect}
        onClick={() => selection.selectAll()}
      >
        Everything
      </button>
      <button
        type="button"
        className="button"
        disabled={!hasSelection}
        onClick={() => selection.clear()}
      >
        Clear
      </button>
      <output className="badge">{summary}</output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <SelectionToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
selection.select({ page: page.ref, start: 20, count: 40 }); // 40 characters on one page
selection.selectWordAt(page.ref, { x: 120, y: 80 }); // the word at a point
selection.selectLineAt(page.ref, { x: 120, y: 80 }); // the line at a point
selection.selectPage(page.ref);
selection.selectAll();
selection.clear();

A search result is already a range, so selecting it takes no conversion:

selection.select(hit); // { page, start, count }

For a selection across pages, give where it starts and where it ends:

selection.select({
  start: { page: firstPage.ref, index: 120 },
  end: { page: lastPage.ref, index: 35 },
});

Ranges include start and stop before end, and they count characters as the page stores them, not JavaScript string positions. selection.extendTo(page.ref, point) grows the current selection to a point on a page, as dragging a handle does. Every method that takes a page takes its ref or its index: selection.selectPage(0) selects the first page.

Show the selection in your UI#

useSelectionState() gives you what to show, and your component re-renders when it changes:

highlight-button.tsx
import { useSelectionState } from '@embedpdf/react/selection';

export function HighlightButton() {
  const { hasSelection, isSelecting, pages } = useSelectionState();

  return <button disabled={!hasSelection}>Highlight</button>;
}

Loading live preview…

import { useEffect } from 'react';
import { Viewer, DocumentGate, usePageList } 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 {
  SelectionLayer,
  selectionPlugin,
  useSelection,
  useSelectionState,
} from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './state.css';

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

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

// Every field of the selection's state, as it changes.
function SelectionStatus() {
  const { hasSelection, isSelecting, range, pages } = useSelectionState();

  return (
    <dl className="status">
      <div className="field">
        <dt>hasSelection</dt>
        <dd>{String(hasSelection)}</dd>
      </div>
      <div className="field" data-live={isSelecting}>
        <dt>isSelecting</dt>
        <dd>{String(isSelecting)}</dd>
      </div>
      <div className="field">
        <dt>pages</dt>
        <dd>{pages.length === 1 ? '1 page' : `${pages.length} pages`}</dd>
      </div>
      <div className="field">
        <dt>range</dt>
        <dd>{range ? `${range.start.index} → ${range.end.index}` : 'null'}</dd>
      </div>
    </dl>
  );
}

// Something selected on load: the title on the cover.
function SelectTitle() {
  const selection = useSelection();
  const cover = usePageList()[0]?.ref;

  useEffect(() => {
    if (cover) selection.select({ page: cover, start: 10, count: 52 });
  }, [selection, cover]);

  return null;
}

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

Try it: drag over text. isSelecting is true until you let go, and pages counts the pages the selection reaches.

Every field is listed under State. For drawing your own highlight, selection.listSegments(page.ref) gives you each selected line as four corners, turned with the text, and selection.listRects(page.ref) a plain box per line.

Copy the selected text#

Reading the text and using the clipboard are two steps:

const text = await selection.readText();

readText() reads only the selected characters and gives them to you; it doesn’t touch the clipboard. Lines on different pages are joined with a new line, and with nothing selected it’s an empty string. readTextInRange(range) reads any range without selecting it.

For the clipboard, the React package has two helpers. copySelection() copies from your own button:

copy-button.tsx
import { copySelection, useSelection, useSelectionState } from '@embedpdf/react/selection';

export function CopyButton() {
  const selection = useSelection();
  const { hasSelection } = useSelectionState();

  return (
    <button disabled={!hasSelection || !selection.canCopy()} onClick={() => copySelection(selection)}>
      Copy
    </button>
  );
}

<SelectionClipboard> makes Ctrl+C and the browser’s Copy menu work. Mount it once per document view:

clipboard.tsx
import { SelectionClipboard } from '@embedpdf/react/selection';

// Once per document view: Ctrl+C / Cmd+C and the browser's Copy menu
export const Clipboard = () => <SelectionClipboard />;

It reads the text as soon as a selection settles, so the browser’s copy can answer at once. Set prefetch to false to read text only when someone copies. A selection the user may not copy isn’t copied by the keys or by the browser’s Copy menu, and onRefused is called, so you can say so.

With more than one viewer on a page, pass target, a ref to the element around one: Ctrl+C and the Copy menu then copy its selection only while focus is inside it, and the page keeps its own copying everywhere else.

Loading live preview…

import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, usePageList } 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 {
  SelectionClipboard,
  SelectionLayer,
  copySelection,
  selectionPlugin,
  useSelection,
  useSelectionState,
} from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './copy.css';

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

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 CopyButton() {
  const selection = useSelection();
  const hasSelection = useSelectionState((state) => state.hasSelection);
  const [status, setStatus] = useState('');

  const copy = () => {
    copySelection(selection).then(
      (text) => setStatus(`Copied ${text.length} characters`),
      // The browser can refuse the clipboard, for example in a frame that doesn't allow it.
      () => setStatus("The browser didn't allow copying"),
    );
  };

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={!hasSelection || !selection.canCopy()}
        onClick={copy}
      >
        Copy
      </button>
      <output className="readout">{status || 'Or press Ctrl+C'}</output>
    </div>
  );
}

// The selected text, read with readText() each time the selection settles.
function SelectedText() {
  const selection = useSelection();
  const { range, isSelecting } = useSelectionState();
  const [text, setText] = useState('');

  useEffect(() => {
    if (isSelecting || !selection.canCopy()) return;
    // A newer selection cancels a read that hasn't finished.
    const controller = new AbortController();
    selection.readText({ signal: controller.signal }).then(setText, () => {});
    return () => controller.abort();
  }, [selection, range, isSelecting]);

  return <p className="preview">{text || 'Select some text to read it here.'}</p>;
}

// Something selected on load: the title on the cover.
function SelectTitle() {
  const selection = useSelection();
  const cover = usePageList()[0]?.ref;

  useEffect(() => {
    if (cover) selection.select({ page: cover, start: 10, count: 52 });
  }, [selection, cover]);

  return null;
}

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

Try it: select other text: the line above the pages shows what readText() reads once the selection settles. Then press Copy, or Ctrl+C.

Add a selection menu#

<SelectionMenu> places your own UI next to the selection. It hides while the user drags and appears when the selection settles:

Loading live preview…

import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, usePageList } 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 {
  SelectionClipboard,
  SelectionLayer,
  SelectionMenu,
  copySelection,
  selectionPlugin,
  useSelection,
} from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './menu.css';

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

const PLACEMENTS = ['top', 'bottom', 'left', 'right'] as const;
type Placement = (typeof PLACEMENTS)[number];

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

// What's in the menu is yours: here, copy and clear.
function SelectionActions() {
  const selection = useSelection();

  const copy = () => {
    void copySelection(selection).catch(() => {
      // Show your product's clipboard error message here.
    });
  };

  return (
    <div className="menu">
      {selection.canCopy() && (
        <button type="button" className="button" onClick={copy}>
          Copy
        </button>
      )}
      <button type="button" className="button" onClick={() => selection.clear()}>
        Clear
      </button>
    </div>
  );
}

// Something selected on load, so the menu shows: the title on the cover.
function SelectTitle() {
  const selection = useSelection();
  const cover = usePageList()[0]?.ref;

  useEffect(() => {
    if (cover) selection.select({ page: cover, start: 10, count: 52 });
  }, [selection, cover]);

  return null;
}

export default function App() {
  const [placement, setPlacement] = useState<Placement>('top');

  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <SelectTitle />
        <SelectionClipboard />
        <div className="toolbar" role="group" aria-label="Where the menu goes">
          {PLACEMENTS.map((name) => (
            <button
              key={name}
              type="button"
              className="segment"
              aria-pressed={placement === name}
              onClick={() => setPlacement(name)}
            >
              {name}
            </button>
          ))}
        </div>
        <Stage
          className="stage"
          overlay={
            <SelectionMenu placement={placement} gap={8}>
              <SelectionActions />
            </SelectionMenu>
          }
        >
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Try it: select other text, and the menu follows it. Pick where it goes above the pages.

Mount it in the Stage’s overlay, and it stays attached to the text while the user scrolls and zooms. The menu only places things; what’s in it is yours: copy, comment, highlight, redact. placement is 'top', 'bottom', 'left' or 'right', and gap the space in pixels.

On touch screens#

A finger has no text cursor, so selection works as it does elsewhere on a phone: long-press a word, then drag the handles. Add <SelectionHandles> next to your menu:

selection-handles.tsx
import { RenderLayer } from '@embedpdf/react/render';
import { SelectionHandles, SelectionLayer, SelectionMenu } from '@embedpdf/react/selection';
import { Stage } from '@embedpdf/react/stage';

import { CopyButton } from './copy-button';

export function Pages() {
  return (
    <Stage
      overlay={
        <>
          <SelectionMenu>
            <CopyButton />
          </SelectionMenu>
          <SelectionHandles />
        </>
      }
    >
      {() => (
        <>
          <RenderLayer />
          <SelectionLayer />
        </>
      )}
    </Stage>
  );
}

Loading live preview…

import { useEffect } from 'react';
import { Viewer, DocumentGate, usePageList } 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 {
  SelectionHandles,
  SelectionLayer,
  SelectionMenu,
  copySelection,
  selectionPlugin,
  useSelection,
} from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './handles.css';

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

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 CopyButton() {
  const selection = useSelection();

  return (
    <button
      type="button"
      className="button"
      disabled={!selection.canCopy()}
      onClick={() => void copySelection(selection).catch(() => {})}
    >
      Copy
    </button>
  );
}

// A word selected on load, as a long-press selects one: "Viewers", on the cover.
function SelectWord() {
  const selection = useSelection();
  const cover = usePageList()[0]?.ref;

  useEffect(() => {
    if (cover) selection.select({ page: cover, start: 14, count: 7 });
  }, [selection, cover]);

  return null;
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <SelectWord />
        <Stage
          className="stage"
          overlay={
            <>
              {/* Clear of the start handle's grip, which sits above the first line. */}
              <SelectionMenu gap={20}>
                <CopyButton />
              </SelectionMenu>
              <SelectionHandles />
            </>
          }
        >
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Try it: drag a handle’s round grip, with a finger or the mouse: the selection grows or shrinks from the other end.

Each handle is the selection’s own edge, with a round grip. Dragging one grows the selection from the other end, across pages too. Grabbing a handle never scrolls the page, and everywhere else a finger still scrolls and pinch-zooms. For the small tick a native app gives on long-press, see haptic feedback.

Methods#

Everything useSelection() gives you.

MethodWhat it does
select(range)Selects { page, start, count } on one page, or { start, end } across pages. An empty range clears. Select text from code
selectWordAt(page, point)Selects the word at a point, like a double-click. Returns false when there’s no text there.
selectLineAt(page, point)Selects the line at a point, like a triple-click. Returns false when there’s no text there.
selectPage(page)Selects every character on a page
selectAll()Selects the whole document
extendTo(page, point)Grows the selection to a point, as dragging a handle does
clear()Clears the selection. Always allowed.
readText()The selected text, or '' with nothing selected. Needs permission to copy. Copy the selected text
readTextInRange(range)The text of any range, without selecting it
listSegments(page)Each selected line on a page, as four corners turned with the text
listRects(page)A plain box around each selected line on a page
getAnchor()Where a menu attaches: { page, bounds } on the page where the selection ends, or null. <SelectionMenu> uses it.
getSnapshot()Everything at once: each page’s lines, both ends with their direction, and the range. For drawing your own highlight and handles.
canSelect(), canCopy()Whether selecting, and reading the selected text, are allowed. 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 useSelectionSettings(). Settings

Every method that returns a promise also takes a signal in its options, to cancel it.

State#

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

FieldWhat it isRead once
hasSelectionWhether anything is selectedhasSelection()
isSelectingWhether the user is still draggingisSelecting()
rangeThe selection as { start, end }, to store and pass back to select(), or nullgetRange()
pagesThe pages the selection is on, in document orderlistSelectedPages()

Settings#

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

SettingWhat it doesDefaultFrom CSS
dragThresholdHow far, in pixels, the pointer moves before a drag selects4—
colorThe color of selected textthe viewer’s accent at 35%--epdf-text-selection
handles.colorThe grips at its ends, on touch screensthe viewer’s accent--epdf-text-selection-handle
handles.shadowTheir shadow'0 1px 4px rgb(0 0 0 / 0.35)'--epdf-text-selection-handle-shadow

A CSS variable wins over the setting, for dark mode or a different look per viewer: see Colors & theming.

Loading live preview…

import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, usePageList } 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 {
  SelectionHandles,
  SelectionLayer,
  selectionPlugin,
  useSelection,
  useSelectionSettings,
} from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './colors.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin({ handles: { shadow: '0 1px 3px rgb(0 0 0 / 0.3)' } }),
];

// Each: the selected text and its handles. `null` is the viewer's accent: at 35% for the text.
const COLORS = [
  { name: 'Accent', color: null, handles: null, swatch: '#3858e9' },
  { name: 'Yellow', color: 'rgb(250 204 21 / 0.45)', handles: '#ca8a04', swatch: '#facc15' },
  { name: 'Green', color: 'rgb(34 197 94 / 0.35)', handles: '#16a34a', swatch: '#22c55e' },
  { name: 'Pink', color: 'rgb(236 72 153 / 0.3)', handles: '#db2777', swatch: '#ec4899' },
];

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 ColorPicker() {
  const selection = useSelection();
  const current = useSelectionSettings((settings) => settings.color);

  return (
    <>
      {COLORS.map(({ name, color, handles, swatch }) => (
        <button
          key={name}
          type="button"
          className="swatch"
          aria-label={name}
          aria-pressed={current === color}
          style={{ background: swatch }}
          onClick={() => selection.updateSettings({ color, handles: { color: handles } })}
        />
      ))}
      <button type="button" className="button" onClick={() => selection.resetSettings()}>
        Reset
      </button>
    </>
  );
}

// Something selected on load: the title on the cover.
function SelectTitle() {
  const selection = useSelection();
  const cover = usePageList()[0]?.ref;

  useEffect(() => {
    if (cover) selection.select({ page: cover, start: 10, count: 52 });
  }, [selection, cover]);

  return null;
}

export default function App() {
  const [fromCss, setFromCss] = useState(false);

  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <SelectTitle />
        <div className="toolbar">
          <ColorPicker />
          <label className="option">
            <input
              type="checkbox"
              checked={fromCss}
              onChange={(event) => setFromCss(event.target.checked)}
            />
            Override with CSS
          </label>
        </div>
        <Stage className={fromCss ? 'stage from-css' : 'stage'} overlay={<SelectionHandles />}>
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Try it: pick a color, and selection.updateSettings({ color, handles }) changes the selection and its handles at once. Then turn on Override with CSS: the CSS variables win over the color you picked.

Events#

selection-committed.tsx
import { useSelectionEvent } from '@embedpdf/react/selection';

import { prefetchTranslation } from './translation';

export function TranslationPrefetch() {
  useSelectionEvent(
    (selection) => selection.onCommitted,
    () => prefetchTranslation(),
  );

  return null;
}
EventFires whenCarries
onChangedThe selection changed, while dragging toorange, pages
onCommittedA drag or a tap finished selectingrange
onClearedThe selection was cleared—
onSettingsChangedThe settings changedsettings, changed

Use the change event for things that follow the selection as it grows, and the commit event for things that should happen once, such as creating a highlight.

Permissions#

Selecting and copying are two permissions:

PermissionWhat it allowsCheck
doc.text.selectSelecting text, and highlighting itselection.canSelect()
doc.text.copyReading the selected text with readText()selection.canCopy()

Without doc.text.select, pointer selection does nothing and select() is refused; clear() always works. Without doc.text.copy, people can still select and highlight, but readText() is refused and no page text is sent. So a document can allow markup without copy.

Good to know#

Characters, not string positions#

A range counts characters as the page stores them. Search results, annotations and selections all use the same counting, so a range from one goes straight into another.

No HTML text layer#

The selection works from where each character is, so there’s no hidden HTML text over the page. That keeps large documents fast, and it’s why a document can allow selecting without giving away its text.

Outside the browser#

The selection plugin itself has no clipboard or HTML in it, so readText() also works in tests and in Node. The clipboard is the React package’s job.

Was this page helpful?

Your feedback goes directly to the documentation team.