EmbedPDF

Links

Most PDFs have links: a table of contents that jumps to a chapter, a footnote to a website. Register linkPlugin() and put <LinkLayer> in each page, and they work:

link-layer.tsx
import { interactionPlugin } from '@embedpdf/react/interaction';
import { LinkLayer, linkPlugin } from '@embedpdf/react/link';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { Viewer } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { engine } from './pdf';

const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), linkPlugin()];

export function Reader() {
  return (
    <Viewer engine={engine} plugins={plugins}>
      <Stage>
        {() => (
          <>
            <RenderLayer />
            <LinkLayer />
          </>
        )}
      </Stage>
    </Viewer>
  );
}

Loading live preview…

import { useEffect, useRef } 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 { annotationPlugin, useAnnotation } from '@embedpdf/react/annotation';
import { LinkLayer, linkPlugin } from '@embedpdf/react/link';
import { localEngine } from '@embedpdf/engine';

import './basic.css';

const engine = localEngine();
// The annotation plugin is only here to make the links below.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  linkPlugin(),
];

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

// Where the two links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };

// Made on load, since the document has none: a label the page shows, and a link over it.
function AddLinks() {
  const annotation = useAnnotation();
  const pages = usePageList();
  const added = useRef(false);

  useEffect(() => {
    const [first, , third] = pages;
    if (!first || !third || added.current) return;
    added.current = true;
    const label = (text: string, box: typeof TO_PAGE_3) =>
      annotation.create(first.ref, {
        subtype: 'free-text',
        box,
        contents: text,
        fontSize: 13,
        fontColor: '#054fb3',
        interiorColor: '#e8f1ff',
        color: '#7db6ff',
        strokeWidth: 1,
      });
    void (async () => {
      await label('Go to page 3 →', TO_PAGE_3);
      await label('Open embedpdf.com ↗', TO_WEBSITE);
      await annotation.create(first.ref, {
        subtype: 'link',
        rect: TO_PAGE_3,
        target: { kind: 'goto', destination: { kind: 'fit', page: third.ref } },
      });
      await annotation.create(first.ref, {
        subtype: 'link',
        rect: TO_WEBSITE,
        target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
      });
    })();
  }, [annotation, pages]);

  return null;
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddLinks />
        <p className="hint">
          Two links at the top of the first page. Click one, or press Tab to reach it and Enter to
          follow it.
        </p>
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <LinkLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
  • A link to a page goes there, to the exact spot and zoom the link names.
  • A link to a website opens in a new tab, after a safety check (see Good to know).
  • A link with a PDF action, such as a button that resets a form, runs the action when the actions plugin is registered.

The pointer turns into a hand over a link, and a link can be reached with the Tab key and followed with Enter.

By default a link area is invisible, as in other PDF apps. <LinkLayer renderLink> lets you draw it yourself, for example with an underline on hover:

custom-links.tsx
import { LinkLayer } from '@embedpdf/react/link';

export const Links = () => (
  <LinkLayer
    renderLink={({ link, native }) => (
      <span className="pdf-link" title={link.target.kind === 'uri' ? link.target.uri : undefined}>
        {native}
      </span>
    )}
  />
);

Loading live preview…

import { useEffect, useRef, 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 { annotationPlugin, useAnnotation } from '@embedpdf/react/annotation';
import { LinkLayer, linkPlugin } from '@embedpdf/react/link';
import { localEngine } from '@embedpdf/engine';

import './custom.css';

const engine = localEngine();
// The annotation plugin is only here to make the links below.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  linkPlugin(),
];

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

// Where the two links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };

// Made on load, since the document has none: a label the page shows, and a link over it.
function AddLinks() {
  const annotation = useAnnotation();
  const pages = usePageList();
  const added = useRef(false);

  useEffect(() => {
    const [first, , third] = pages;
    if (!first || !third || added.current) return;
    added.current = true;
    const label = (text: string, box: typeof TO_PAGE_3) =>
      annotation.create(first.ref, {
        subtype: 'free-text',
        box,
        contents: text,
        fontSize: 13,
        fontColor: '#054fb3',
        interiorColor: '#e8f1ff',
        color: '#7db6ff',
        strokeWidth: 1,
      });
    void (async () => {
      await label('Go to page 3 →', TO_PAGE_3);
      await label('Open embedpdf.com ↗', TO_WEBSITE);
      await annotation.create(first.ref, {
        subtype: 'link',
        rect: TO_PAGE_3,
        target: { kind: 'goto', destination: { kind: 'fit', page: third.ref } },
      });
      await annotation.create(first.ref, {
        subtype: 'link',
        rect: TO_WEBSITE,
        target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
      });
    })();
  }, [annotation, pages]);

  return null;
}

export default function App() {
  const [shown, setShown] = useState(true);

  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddLinks />
        <div className="toolbar">
          <div className="segmented" role="group" aria-label="Link areas">
            <button type="button" aria-pressed={shown} onClick={() => setShown(true)}>
              Show link areas
            </button>
            <button type="button" aria-pressed={!shown} onClick={() => setShown(false)}>
              As the PDF has them
            </button>
          </div>
        </div>
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <LinkLayer
                renderLink={
                  ({ link, native }) =>
                    shown ? (
                      // Wrapping `native` keeps what a click does.
                      <span className="pdf-link" data-kind={link.target.kind}>
                        {native}
                      </span>
                    ) : undefined // the layer's own, invisible area
                }
              />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

native is the layer’s own clickable area: wrap it to keep what a click does. Return undefined for a link you don’t want to change.

link.listLinks(page.ref); // every link on the page
link.getLinkAt(page.ref, { x: 120, y: 80 }); // the link at a point, or null

Each link has an id, its bounds on the page, and a target: where it goes.

target.kindGoes to
'goto'A page of this document: target.destination
'uri'A website or an email address: target.uri
'named'A viewer action, such as the next page
'javascript'Runs the link’s script, when the actions plugin allows it
'goto-remote', 'launch'Nowhere: see Which links never run

activate() does what a click does:

const { outcome } = link.activate(someLink);

It returns what happened right away, so a website can still open in a new tab: 'revealed' (the view went to a page), 'uri' (a website opened), or 'dispatched' (the actions plugin runs the link’s action). A link it won’t follow, such as one to another file, is 'reported'.

Loading live preview…

import { useCallback, useEffect, useRef, 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 { annotationPlugin, useAnnotation } from '@embedpdf/react/annotation';
import { LinkLayer, linkPlugin, useLink, useLinkEvent } from '@embedpdf/react/link';
import { localEngine } from '@embedpdf/engine';

import './list.css';

const engine = localEngine();
// The annotation plugin is only here to make the links below.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  linkPlugin(),
];

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

// Where the links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };
const TO_SCRIPT = { x: 458, y: 24, width: 82, height: 28 };

// Made on load, since the document has none: a label the page shows, and a link over it.
// The third one is a `javascript:` address, which a link never opens.
function AddLinks({ onAdded }: { onAdded: () => void }) {
  const annotation = useAnnotation();
  const pages = usePageList();
  const added = useRef(false);

  useEffect(() => {
    const [first, , third] = pages;
    if (!first || !third || added.current) return;
    added.current = true;
    const label = (text: string, box: typeof TO_PAGE_3) =>
      annotation.create(first.ref, {
        subtype: 'free-text',
        box,
        contents: text,
        fontSize: 13,
        fontColor: '#054fb3',
        interiorColor: '#e8f1ff',
        color: '#7db6ff',
        strokeWidth: 1,
      });
    void (async () => {
      await label('Go to page 3 →', TO_PAGE_3);
      await label('Open embedpdf.com ↗', TO_WEBSITE);
      await label('A script', TO_SCRIPT);
      await annotation.create(first.ref, {
        subtype: 'link',
        rect: TO_PAGE_3,
        target: { kind: 'goto', destination: { kind: 'fit', page: third.ref } },
      });
      await annotation.create(first.ref, {
        subtype: 'link',
        rect: TO_WEBSITE,
        target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
      });
      await annotation.create(first.ref, {
        subtype: 'link',
        rect: TO_SCRIPT,
        target: { kind: 'uri', uri: 'javascript:alert(1)' },
      });
      onAdded();
    })();
  }, [annotation, pages, onAdded]);

  return null;
}

// The first page's links, read from the plugin, each with a button that follows it.
function LinkList({ ready }: { ready: boolean }) {
  const link = useLink();
  const [last, setLast] = useState<string | null>(null);

  // Every way of following a link ends up here: a click, a key, or code.
  useLinkEvent(
    (link) => link.onActivated,
    ({ target, activation }) => setLast(`${target.kind} → ${activation.outcome}`),
  );

  const links = ready ? link.listLinks(0) : [];

  return (
    <aside className="panel">
      <h3 className="heading">Links on page 1</h3>
      <ul className="links">
        {links.map((item) => (
          <li key={item.id} className="link">
            <span className="kind">{item.target.kind}</span>
            <span className="label">{link.getLabel(item)}</span>
            <span className="where">
              at {Math.round(item.bounds.x)}, {Math.round(item.bounds.y)}
            </span>
            <button type="button" className="button" onClick={() => link.activate(item)}>
              Follow
            </button>
          </li>
        ))}
      </ul>
      <p className="last">
        <code>onActivated</code> {last ?? 'not yet'}
      </p>
    </aside>
  );
}

export default function App() {
  // The list reads the links once they're made.
  const [ready, setReady] = useState(false);
  const onAdded = useCallback(() => setReady(true), []);

  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddLinks onAdded={onAdded} />
        <div className="layout">
          <LinkList ready={ready} />
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <LinkLayer />
              </>
            )}
          </Stage>
        </div>
      </DocumentGate>
    </Viewer>
  );
}

Methods#

Everything useLink() gives you.

MethodWhat it does
listLinks(page)Every link on a page, once the page’s links are read. Read the links on a page
getLink(page, id)One link by its id, or null
getLinkAt(page, point)The link at a point on the page, the smallest when links overlap, or null
listAllLinks()Every link in the document, reading each page’s links as needed
ensureLoaded(page)Reads a page’s links if they aren’t read yet. <LinkLayer> does this for every page it shows.
isLoaded(page), getStatus(page)Whether a page’s links are read, and in detail: 'idle', 'loading', 'ready', 'error' or 'forbidden'
activate(link)Follows a link, or a link’s target, as a click does, and returns what happened. Follow a link from code
activateAt(page, point)Follows the link at a point on the page. Returns null when there’s no link there.
resolve(target)What following a target would do, without doing it: { kind } is 'destination', 'uri', 'named' or 'reported'
getLabel(link)A short label for a tooltip: the web address, or where the link goes

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

Events#

link-analytics.tsx
import { LinkLayer, useLinkEvent } from '@embedpdf/react/link';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { analytics } from './analytics';

export function Reader() {
  useLinkEvent(
    (link) => link.onActivated,
    ({ target }) => analytics.track('link_followed', { kind: target.kind }),
  );

  return (
    <Stage>
      {() => (
        <>
          <RenderLayer />
          <LinkLayer />
        </>
      )}
    </Stage>
  );
}
EventFires whenCarries
onActivatedSomeone followed a link, by click, key or codetarget, activation
onLoadedA page’s links were read, or read again after a changepage

Good to know#

A website link opens only with http, https, mailto or tel. An address without one opens on https when it starts with // or www.; any other, such as /help or page2.html, is relative to the document and opens nothing, so a link can never open a page of your own app. Links that would start a program or open another file on the user’s computer are never followed, and a link’s JavaScript runs only when the actions plugin has JavaScript turned on.

Links work while the pointer or pan tool is active. With the annotation plugin’s link tool, the layer steps aside and links become annotations you can move, resize and retarget; see Annotations.

Links are annotations: create them with the annotation plugin, or with the engine; see Links.

Was this page helpful?

Your feedback goes directly to the documentation team.