Your own components
Put your own component in a toolbar, in a panel, or above and below the viewer. It stays a component of your app: your framework renders it, your CSS styles it, and it reads your app’s state as it always does. The viewer only decides where it goes.
Two steps: make room for it in the layout, then give the viewer the component. This one puts a version picker from your app in the toolbar:
import { PDFViewer, ToolbarItem, type Layout } from '@embedpdf/viewer-react';
import { useVersions, type Version } from './versions'; // your app
const layout = (layout: Layout) =>
layout.add({ custom: 'versions', command: 'acme:versions' }, { to: 'main', section: 'start' });
function VersionPicker() {
const { versions, current, select } = useVersions();
return (
<select className="versions" value={current} onChange={(event) => select(event.target.value)}>
{versions.map((version: Version) => (
<option key={version.id} value={version.id}>
{version.label}
</option>
))}
</select>
);
}
export function Contract() {
const { currentUrl } = useVersions();
return (
<PDFViewer src={currentUrl} layout={layout} style={{ height: '100vh' }}>
<ToolbarItem id="versions">
<VersionPicker />
</ToolbarItem>
</PDFViewer>
);
}{ custom: 'versions' } makes room in the toolbar, and the id ties it to your component. The
toolbar measures your component like any button, so it fits it in, and when the toolbar runs out of
room, command takes its place in the “More” menu: here, a command of yours that opens the same
choice in a dialog. Leave command out, and your component stays in the toolbar while other items
make room.
Know where it sits#
The same component can sit in a top bar, a vertical rail or a floating bar. useToolbarItem() tells
it where it is, so it can lay itself out:
import { useToolbarItem } from '@embedpdf/viewer-react';
export function Reviewers({ people }: { people: { id: string; name: string; avatar: string }[] }) {
const { orientation } = useToolbarItem(); // 'horizontal' or 'vertical'
return (
<div className={`reviewers ${orientation}`}>
{people.map((person) => (
<img key={person.id} src={person.avatar} alt={person.name} title={person.name} />
))}
</div>
);
}| Field | What it is |
|---|---|
orientation | 'horizontal' or 'vertical' |
edge | The bar’s place: 'top', 'bottom', 'start', 'end', 'float' or 'selection' |
variant | Which of your variants the toolbar picked, when you gave some |
size | The viewer’s size: 'compact', 'medium' or 'expanded' |
The same values are on your element as data-orientation, data-edge, data-variant and
data-size, for CSS.
To have the toolbar pick a smaller form when it’s short of room, list your forms, largest first:
{ custom: 'versions', variants: ['full', 'short'], command: 'acme:versions' }. Draw the one
variant names.
Look like the viewer#
Button, Menu and MenuItem are the viewer’s own, for your components. A menu made with them
opens the way the viewer’s menus do: away from its bar, flipped when there’s no room, and up from
the bottom on a phone.
import { Menu, MenuItem } from '@embedpdf/viewer-react';
import { copyLink, emailLink } from './share'; // your app
export function ShareMenu() {
return (
<Menu icon="link" label="Share">
<MenuItem icon="copy" onSelect={copyLink}>
Copy link
</MenuItem>
<MenuItem icon="message" onSelect={emailLink}>
Email a link
</MenuItem>
</Menu>
);
}| Component | What it is |
|---|---|
Button | A toolbar button. command="zoom:in" makes it that command, with its icon, tooltip and state; or give it icon, label and a click handler. |
Menu | A button that opens a menu: icon, label, and MenuItems inside |
MenuItem | A row in a menu: icon, its text, and what happens when it’s picked. command="…" makes it a command. |
Icons are the viewer’s icons and yours, by name. For anything else, the viewer’s
colors are CSS variables your styles can use: var(--epdf-surface), var(--epdf-text),
var(--epdf-border), var(--epdf-accent). See Theming.
Use the viewer’s commands and state#
useCommand(id) gives you a command as a button needs it: its label, icon and shortcut,
whether it’s enabled, active and visible, and run(). It’s always there: an id the viewer
doesn’t have is neither visible nor enabled. And every hook from the headless
docs works inside the viewer, so your component reads the viewer’s state the same
way a headless app does:
import { Button, useCommand } from '@embedpdf/viewer-react';
import { useStageState } from '@embedpdf/react/stage';
export function ZoomPill() {
const { zoomLevel } = useStageState();
const fitWidth = useCommand('zoom:fit-width');
return (
<div className="zoom-pill">
<Button command="zoom:out" />
<span>{Math.round(zoomLevel * 100)}%</span>
<Button command="zoom:in" />
<button className="chip" aria-pressed={fitWidth.active} onClick={fitWidth.run}>
{fitWidth.label}
</button>
</div>
);
}useStageState() is the Stage plugin’s state, from the
headless adapter for your framework. Install it next to the viewer (@embedpdf/react,
@embedpdf/vue, @embedpdf/svelte or @embedpdf/angular) and use any of its hooks: the search
results, the selected annotation, the form values.
A panel of your own#
Add the panel to the layout with a side, a title and an icon, then give the viewer what goes inside:
import { PDFViewer, Panel, type Layout } from '@embedpdf/viewer-react';
import { Notes } from './notes'; // your app
const layout = (layout: Layout) =>
layout
.addPanel('notes', { side: 'end', title: 'Notes', icon: 'book' })
.add('panel:notes', { to: 'main', section: 'end' });
export function Review() {
return (
<PDFViewer src="/report.pdf" layout={layout} style={{ height: '100vh' }}>
<Panel id="notes">
<Notes />
</Panel>
</PDFViewer>
);
}Every panel gets a panel:<id> command that opens and closes it, so panel:notes is the toolbar
button. The viewer draws the panel’s frame: the header with its title and close button, and the way
it opens, beside the pages on a wide screen and up from the bottom on a phone. You draw what’s
inside. See Panels.
Above, below, and when nothing is open#
Regions are places around the viewer for your own components:
import { PDFViewer, Region } from '@embedpdf/viewer-react';
import { AppHeader, RecentFiles } from './app'; // your app
export function Workspace() {
return (
<PDFViewer style={{ height: '100vh' }}>
<Region name="header">
<AppHeader />
</Region>
<Region name="empty">
<RecentFiles />
</Region>
</PDFViewer>
);
}| Region | Where it is |
|---|---|
header | Above everything, across the whole viewer |
footer | Below everything |
tabs | In place of the document tabs, for your own |
empty | Over the pages while no document is open |
A region you don’t fill shows nothing, or the viewer’s own (the tabs).
In the menu over selected text#
To add an action to the bar that appears over selected text, such as “Explain this”, make it a command and add it there:
{
commands: [
{
id: 'acme:explain',
label: 'Explain',
icon: 'message',
run: async ({ get }) => openAssistant(await get('selection').readText()),
},
],
layout: (layout) => layout.add('acme:explain', { to: 'selection' }),
}The bar over selected annotations is annotation. See Menus.
Good to know#
Your styles apply, the viewer’s don’t#
Your component lives in your page, not inside the viewer’s shadow DOM. Your CSS styles it, and the viewer’s CSS never touches it. It does inherit the viewer’s font and text color and its CSS variables, so plain elements fit in without any styling.
One id, one component#
Each id is one item in one place. To show the same component in two bars, give each place its
own id.
Keyboard#
Your item is part of the toolbar’s keyboard order. Arrow keys move to it like any button; the first
element in it that takes focus gets it. Inside your item, keys work as you wrote them: the viewer’s
shortcuts never fire while focus is in a text field or a <select>.
Plain HTML#
Without a framework, give the element a slot attribute: item:<id>, panel:<id>, or the
region’s name. The viewer’s components are elements too: <epdf-button>, <epdf-menu> and
<epdf-menu-item>, which fire a select event when picked.
Your feedback goes directly to the documentation team.