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 { Viewer, DocumentGate } 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 { actionsPlugin, useActionsUiAdapter } from '@embedpdf/react/actions';
import { LinkLayer, linkPlugin } from '@embedpdf/react/link';
import { localEngine } from '@embedpdf/engine';
import './basic.css';
const engine = localEngine();
// The actions plugin runs a link that names a viewer command, such as the footer's Next.
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), actionsPlugin(), linkPlugin()];
const links = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/links.pdf');
return { kind: 'bytes', id: 'links', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// The browser's way to open a website, once for the viewer.
function BrowserActions() {
useActionsUiAdapter();
return null;
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: links }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<BrowserActions />
<p className="hint">
Every blue label is a link. 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 { useState } from 'react';
import { Viewer, DocumentGate } 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 { actionsPlugin, useActionsUiAdapter } from '@embedpdf/react/actions';
import { LinkLayer, linkPlugin } from '@embedpdf/react/link';
import { localEngine } from '@embedpdf/engine';
import './custom.css';
const engine = localEngine();
// The actions plugin runs a link that names a viewer command, such as the footer's Next.
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), actionsPlugin(), linkPlugin()];
const links = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/links.pdf');
return { kind: 'bytes', id: 'links', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// The browser's way to open a website, once for the viewer.
function BrowserActions() {
useActionsUiAdapter();
return null;
}
export default function App() {
const [shown, setShown] = useState(true);
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: links }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<BrowserActions />
<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 { useEffect, useState } from 'react';
import { Viewer, DocumentGate } 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 { actionsPlugin, useActionsUiAdapter } from '@embedpdf/react/actions';
import { LinkLayer, linkPlugin, useLink, useLinkEvent } from '@embedpdf/react/link';
import type { Link } from '@embedpdf/react/link';
import { localEngine } from '@embedpdf/engine';
import './list.css';
const engine = localEngine();
// The actions plugin runs a link that names a viewer command, such as the footer's Next.
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), actionsPlugin(), linkPlugin()];
const links = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/links.pdf');
return { kind: 'bytes', id: 'links', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// The browser's way to open a website, once for the viewer.
function BrowserActions() {
useActionsUiAdapter();
return null;
}
// The first page's links, read from the plugin, each with a button that follows it.
function LinkList() {
const link = useLink();
const [items, setItems] = useState<readonly Link[]>([]);
const [last, setLast] = useState<string | null>(null);
useEffect(() => {
void link.ensureLoaded(0).then(() => setItems(link.listLinks(0)));
}, [link]);
// 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}`),
);
return (
<aside className="panel">
<h3 className="heading">Links on page 1</h3>
<ul className="links">
{items.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() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: links }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<BrowserActions />
<div className="layout">
<LinkList />
<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.