Buttons & commands
Every button in the viewer runs a command: one action with a label, an icon, a shortcut, and what it does. A toolbar button, a menu row and a key press all run the same command, so they always agree. Add one of yours, then put it in the layout:
{
commands: [
{
id: 'acme:approve',
label: 'Approve',
icon: 'check',
shortcut: 'Mod+Enter',
run: ({ documentId }) => approve(documentId),
},
],
layout: (layout) => layout.add('acme:approve', { to: 'main', section: 'end' }),
}The button’s tooltip says its label and its shortcut the way the platform writes it: Ctrl+Enter,
or ⌘↩ on a Mac. The button moves into the “More” menu when the toolbar is full, and Ctrl+Enter
(Cmd+Enter on a Mac) runs it while the viewer has focus.
Here Approve writes to a status line of your app, above the viewer:
Loading live preview…
import { useState } from 'react';
import { PDFViewer, type ViewerConfig } from '@embedpdf/viewer-react';
import './approve.css';
const welcome = '/pdfs/welcome.pdf';
// A constant: the command calls your app through `appState`, so it never holds an old value.
const config: ViewerConfig<{ approve(): void }> = {
commands: [
{
id: 'acme:approve',
label: 'Approve',
icon: 'check',
shortcut: 'Mod+Enter',
run: ({ appState }) => appState.approve(),
},
],
layout: (layout) => layout.add('acme:approve', { to: 'main', section: 'end' }),
};
export default function App() {
const [status, setStatus] = useState('Waiting for approval');
const approve = () => setStatus(`Approved at ${new Date().toLocaleTimeString()}`);
return (
<div className="app" style={{ height: '100%' }}>
<p className="status">
Status: <strong>{status}</strong>
</p>
<PDFViewer src={welcome} {...config} appState={{ approve }} className="viewer" />
</div>
);
}Try it: click the cover page, press
Ctrl+Enter(Cmd+Enteron a Mac) and watch the status line, then click the check mark at the end of the toolbar. Press?to find Approve among the viewer’s shortcuts.
Define a command#
| Field | What it is |
|---|---|
id | A unique name. Start yours with your own prefix, such as acme:. |
label | What people see, or labelKey for a translated label |
icon | An icon’s name: the viewer’s or your own |
shortcut | A key, such as 'Mod+Shift+S', or a list of keys. Mod is Cmd on a Mac and Ctrl elsewhere. |
shortcutInTextFields | true: its shortcut works in a text field in the viewer too, as Find’s does |
run | What it does |
enabled | When it can run. When it can’t, its button is greyed out. |
active | When it shows as pressed, or checked in a menu |
visible | When it shows at all |
menu | A menu it opens, instead of run |
panel | A panel it opens and closes, instead of run |
requires | The permissions it needs from the scope. Without them, it’s hidden. |
categories | The features it belongs to, so disable turns it off with them. A name of your own makes a new feature. |
A command is the same thing as in headless: see Commands & shortcuts.
Enabled, active and visible#
enabled, active and visible say how a command’s button looks now. They get get, which gives
you any plugin by its name, and appState, your app’s own state. The viewer asks them again
whenever either changes, and after the command runs, so a button is always right:
{
id: 'acme:flag',
label: 'Flag for review',
icon: 'eye',
enabled: ({ get }) => get('selection').hasSelection(),
active: ({ appState, documentId }) => appState.flagged.includes(documentId),
run: ({ appState, documentId }) => appState.toggleFlag(documentId),
}run gets the same: get, appState, the viewer, and the documentId of the document it runs
for.
An enabled, active or visible that throws, because what it reads isn’t there yet, such as a
plugin of a document while none is open, counts as false: the button is greyed out, not pressed,
or hidden until it answers.
Your app’s state#
Give the viewer what your commands read as appState: values, and functions that change them. It
follows its prop like theme does: give it a new value, and every button asks its command again.
Here Approve stays greyed out until every item of the app’s checklist is ticked, and shows as
pressed once the document is approved:
import { useState } from 'react';
import { PDFViewer, type ViewerConfig } from '@embedpdf/viewer-react';
const CHECKS = ['Parties', 'Dates', 'Payment', 'Signatures'];
interface Review {
done: string[];
status: 'review' | 'approved';
approve(): void;
}
// A constant: the commands read your state from `appState`, so nothing goes stale.
const config: ViewerConfig<Review> = {
commands: [
{
id: 'acme:approve',
label: 'Approve',
icon: 'check',
enabled: ({ appState }) => appState.done.length === CHECKS.length,
active: ({ appState }) => appState.status === 'approved',
run: ({ appState }) => appState.approve(),
},
],
layout: (layout) => layout.add('acme:approve', { to: 'main', section: 'end' }),
};
export function ReviewApp() {
const [done, setDone] = useState<string[]>([]);
const [status, setStatus] = useState<Review['status']>('review');
const toggle = (check: string) =>
setDone((done) => (done.includes(check) ? done.filter((c) => c !== check) : [...done, check]));
return (
<>
<fieldset>
{CHECKS.map((check) => (
<label key={check}>
<input type="checkbox" checked={done.includes(check)} onChange={() => toggle(check)} />
{check}
</label>
))}
</fieldset>
<PDFViewer
src="/report.pdf"
{...config}
appState={{ done, status, approve: () => setStatus('approved') }}
style={{ height: '100vh' }}
/>
</>
);
}Because the commands read your state from appState, never from a variable they closed over, they
can be a constant outside your component and never see an old value. Give it a new value when
something changes, as you do with any prop: the viewer sees a new object, not a change inside the
one it has ([...done, item], not done.push(item)). In TypeScript, ViewerConfig<AppState> gives
appState its type in every command.
Change what a built-in button does#
A command with a built-in id replaces the built-in one, everywhere it appears: the button, the menu row and the shortcut. This one makes Download save to your server instead:
{
commands: [
{
id: 'document:download',
run: async ({ viewer, documentId }) => {
const bytes = await viewer.documents.download(documentId);
await fetch(`/api/documents/${documentId}`, { method: 'PUT', body: bytes });
},
},
],
}Give only the fields you change: the label, icon and shortcut stay the built-in ones.
Icons#
An icon is 24 × 24 SVG path data, drawn as a line, so Tabler and Lucide icons paste in as they are. Give a name to each of yours:
{
icons: {
send: ['M10 14L21 3', 'M21 3l-6.5 18a.5.5 0 0 1-1 0L10 14l-7-3.5a.5.5 0 0 1 0-1L21 3z'],
logo: { paths: ['M4 4h16v16H4z'], fill: true },
},
}A string is one path, an array is several, and { paths, fill: true } fills them instead of drawing
their outline. An icon with the name of a built-in one replaces it, everywhere.
Permissions#
A command that needs a permission from the scope says which, with requires. When the user
doesn’t have it, the command is gone from every bar and menu, and its shortcut does nothing:
{
id: 'acme:approve',
label: 'Approve',
icon: 'check',
requires: ['doc.annotate.modify'],
run: ({ documentId }) => approve(documentId),
}The names are the scope’s permissions, listed on Permissions.
The built-in commands already say what they need, so for a user whose scope doesn’t allow
changes the editing tools are gone, and with notAllowed: 'disable' they show greyed out instead,
with the reason in their tooltip. To leave a command out for everyone, disable
it instead: disable: ['acme:approve'].
A command whose setting a plugin locks for the document, such as the facing-pages choices while a plugin shows one page at a time, shows greyed out with the plugin’s reason, and its current choice still checked.
Keyboard shortcuts#
A command’s shortcut works while focus is anywhere inside the viewer, except in a text field,
unless the command says shortcutInTextFields: true, as Find (search:find, Ctrl/Cmd+F)
does. ? opens a sheet with every shortcut. Your shortcut wins over a built-in one with the same
keys. Copying works the same way: Ctrl/Cmd+C, and Copy in the browser’s Edit menu, copy the
viewer’s selected text only while focus is inside it, so the page keeps its own copying.
Run a command from code#
await viewer.commands.execute('zoom:fit-width');
await viewer.commands.execute('panel:search');execute() resolves { status: 'executed' }, or { status: 'rejected', reason } when the command
can’t run now.
Every built-in command#
| Commands | What they do |
|---|---|
document:open, document:download, document:print | Open another file, download, open the Print dialog |
menu:document, menu:view, menu:zoom | Open a built-in menu |
zoom:in, zoom:out, zoom:fit-width, zoom:fit-page, zoom:automatic | Zoom |
zoom:50, zoom:100, zoom:150, zoom:200, zoom:400 | Zoom to a level |
page:next, page:previous, page:first, page:last, page:go-to | Move through pages; the first four show while the document has more than one page |
view:single-page, view:two-pages, view:cover-page | Pages side by side |
view:scroll-vertical, view:scroll-horizontal, view:scroll-paged | Scroll down or across, or one page at a time |
view:rotate-clockwise, view:rotate-counterclockwise, view:fullscreen | Turn the view; fill the screen with the viewer, not your page: see Full screen |
tool:pointer, tool:pan | Select, or drag the pages |
tool:zoom | Drag a box to zoom to it, or click to zoom in (Alt-click: out) |
tool:note, tool:link, tool:highlight, tool:underline, tool:strikeout, tool:squiggly | Comment and mark up |
tool:ink, tool:ink-highlight, tool:free-text, tool:callout, tool:insert-text, tool:replace-text | Draw and write |
tool:square, tool:circle, tool:line, tool:arrow, tool:polygon, tool:polyline | Shapes |
tool:stamp, tool:image, tool:attachment, tool:signature | Insert |
form:design | Switch form design on or off in the Form mode, with its sidebar |
tool:text-field, tool:checkbox, tool:radio, tool:combo-box, tool:list-box, tool:button, tool:signature-field | Build a form |
tool:redact, redaction:apply | Mark areas, then remove them for good |
tool:distance, tool:perimeter, tool:area, measurement:calibrate | Measure |
mode:view, mode:annotate, mode:shapes, mode:insert, mode:form, mode:redact, mode:measure | Switch mode |
panel:<id> | Open or close a panel |
search:find | Open search with the cursor in its field (Ctrl/Cmd+F), with a search panel in the layout |
history:undo, history:redo | Undo and redo |
outline:add, outline:add-child, outline:rename, outline:set-destination, outline:properties, outline:delete | Change the outline: add an item after the selected one, at the place in view (Ctrl/Cmd+B), or inside it; rename it; point it at the place in view; edit its properties; delete it |
outline:expand-all, outline:collapse-all, menu:outline-item | Unfold or fold every outline item; open an outline row’s menu |
attachment:add, attachment:save, attachment:go-to, attachment:delete, menu:attachment-item | Attach a file (args: { files } attach these instead of asking); save, go to or delete the selected file; open a file’s menu |
comment:status-accepted, comment:status-rejected, comment:status-cancelled, comment:status-completed, comment:status-none, comment:delete-thread, menu:comment-item | Set the status of the selected annotation’s comment thread, or delete the thread; open a comment card’s menu |
page:rotate-clockwise, page:rotate-counterclockwise, page:duplicate, page:delete | Act on the selected pages: turn them, copy them in right after themselves, delete them |
page:insert-blank, page:insert-file | Insert a blank page, or the pages of a PDF, after the selected pages, and select what came in; args: { position, files } say where, and which PDFs |
page:extract, page:select-all, menu:page | Save the selected pages as a new PDF; select every page; open the selected pages’ menu |
selection:copy, selection:highlight, selection:underline, selection:strikeout | Act on selected text |
annotation:comment, annotation:style, annotation:link, annotation:group, annotation:ungroup, annotation:delete | Act on selected annotations |
annotation:save-file | Save the file of a selected file attachment |
help:shortcuts | Show every shortcut (?) |
Every tool you add to the annotation plugin gets its
tool:<id> command too.
Good to know#
Tooltips#
Every button in a bar, every mode tab and the buttons in a panel’s header have a tooltip: the button’s label, its shortcut, and why it’s greyed out when it is. A button that shows its label already has a tooltip only when there’s more to say. A greyed-out menu row says why in a tooltip too. The tooltip shows when the pointer rests on the button for a moment, at once when the button gets focus from the keyboard, and never from a touch. Escape hides it. Screen readers read the shortcut and the reason with the button. Its colors are theme tokens: see Theming.
Every other tooltip in the viewer is the same one, with the same timing: those of the buttons in panels and dialogs, and a name too long for its row, such as an attachment’s or an outline item’s, which shows whole while it’s cut short.
The viewer’s icons#
Every icon the viewer has, by name:
alertTriangle, alignBottom, alignCenter, alignLeft, alignMiddle, alignRight, alignTop,
area, arrowBackUp, arrowForwardUp, bold, book, book2, calibrate, callout, check,
chevronDown, chevronLeft, chevronRight, chevronsDown, chevronsUp, circle, circleCheck,
comment, copy, deviceFloppy, distance, dots, doublePage, download, externalLink,
eye, file, fileExport, fileImport, filePlus, fitToPage, fitToWidth, formButton,
formCheckbox, formDesign, formListbox, formRadio, formSelect, formTextfield, freeText,
fullscreen, fullscreenExit, group, hand, highlight, horizontal, inkHighlighter,
insertText, italic, line, lineArrow, link, linkOff, listTree, lock, marqueeSelect,
menu, message, pageByPage, pageRotateClockwise, pageRotateCounterClockwise, palette,
paperclip, pencilMarker, perimeter, photo, plus, pointer, polygon, print, redact, redactArea,
redactCombined, redactionSidebar, replaceText, rotateClockwise, rotateCounterClockwise,
rubberStamp, rubberStampPlus, ruler, save, screenshot, search, send, settings,
sidebar, signature, singlePage, square, squares, squiggly, strikethrough, text,
trash, underline, ungroup, unlock, updateScale, vertical, viewSettings, widgetEdit,
x, zigzag, zoomIn, zoomInArea, zoomOut.
Enabled is for now, permissions are for good#
enabled is for what can’t run right now: nothing selected, nothing to undo. Its button is
greyed out, because the user can change that. What the user may never do here is a permission:
say so with requires, and the button isn’t there at all.
Your feedback goes directly to the documentation team.