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:
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.
Change how links look#
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:
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.
Read the links on a page#
link.listLinks(page.ref); // every link on the page
link.getLinkAt(page.ref, { x: 120, y: 80 }); // the link at a point, or nullEach link has an id, its bounds on the page, and a target: where it goes.
target.kind | Goes 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 |
Follow a link from code#
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.
| Method | What 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#
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>
);
}| Event | Fires when | Carries |
|---|---|---|
onActivated | Someone followed a link, by click, key or code | target, activation |
onLoaded | A page’s links were read, or read again after a change | page |
Good to know#
Which links never run#
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 while editing annotations#
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.
Making links#
Links are annotations: create them with the annotation plugin, or with the engine; see Links.
Your feedback goes directly to the documentation team.