EmbedPDF

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 { Component, signal, ViewEncapsulation } from '@angular/core';
import { EpdfPdfViewer, type ViewerConfig } from '@embedpdf/viewer-angular';

const welcome = '/pdfs/welcome.pdf';

@Component({
  selector: 'demo-root',
  imports: [EpdfPdfViewer],
  styleUrl: './approve.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="app" style="height: 100%">
      <p class="status">
        Status: <strong>{{ status() }}</strong>
      </p>
      <epdf-pdf-viewer [src]="welcome" [config]="config" class="viewer" />
    </div>
  `,
})
export class App {
  readonly welcome = welcome;
  readonly status = signal('Waiting for approval');
  readonly config: ViewerConfig = {
    commands: [
      {
        id: 'acme:approve',
        label: 'Approve',
        icon: 'check',
        shortcut: 'Mod+Enter',
        run: () => this.status.set(`Approved at ${new Date().toLocaleTimeString()}`),
      },
    ],
    layout: (layout) => layout.add('acme:approve', { to: 'main', section: 'end' }),
  };
}

Try it: click the cover page, press Ctrl+Enter (Cmd+Enter on 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#

FieldWhat it is
idA unique name. Start yours with your own prefix, such as acme:.
labelWhat people see, or labelKey for a translated label
iconAn icon’s name: the viewer’s or your own
shortcutA key, such as 'Mod+Shift+S', or a list of keys. Mod is Cmd on a Mac and Ctrl elsewhere.
shortcutInTextFieldstrue: its shortcut works in a text field in the viewer too, as Find’s does
runWhat it does
enabledWhen it can run. When it can’t, its button is greyed out.
activeWhen it shows as pressed, or checked in a menu
visibleWhen it shows at all
menuA menu it opens, instead of run
panelA panel it opens and closes, instead of run
requiresThe permissions it needs from the scope. Without them, it’s hidden.
categoriesThe 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:

app-state.ts
import { Component, computed, signal } from '@angular/core';
import { EpdfPdfViewer, type ViewerConfig } from '@embedpdf/viewer-angular';

const CHECKS = ['Parties', 'Dates', 'Payment', 'Signatures'];

interface Review {
  done: string[];
  status: 'review' | 'approved';
  approve(): void;
}

@Component({
  selector: 'app-review',
  imports: [EpdfPdfViewer],
  template: `
    <fieldset>
      @for (check of checks; track check) {
        <label>
          <input type="checkbox" [checked]="done().includes(check)" (change)="toggle(check)" />
          {{ check }}
        </label>
      }
    </fieldset>
    <epdf-pdf-viewer
      src="/report.pdf"
      [config]="config"
      [appState]="appState()"
      style="height: 100vh"
    />
  `,
})
export class ReviewApp {
  readonly checks = CHECKS;
  readonly done = signal<string[]>([]);
  readonly status = signal<Review['status']>('review');
  readonly appState = computed<Review>(() => ({
    done: this.done(),
    status: this.status(),
    approve: () => this.status.set('approved'),
  }));

  toggle(check: string) {
    this.done.update((done) =>
      done.includes(check) ? done.filter((c) => c !== check) : [...done, check],
    );
  }

  // The commands read your state from `appState`, so nothing goes stale.
  readonly 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' }),
  };
}

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#

CommandsWhat they do
document:open, document:download, document:printOpen another file, download, open the Print dialog
menu:document, menu:view, menu:zoomOpen a built-in menu
zoom:in, zoom:out, zoom:fit-width, zoom:fit-page, zoom:automaticZoom
zoom:50, zoom:100, zoom:150, zoom:200, zoom:400Zoom to a level
page:next, page:previous, page:first, page:last, page:go-toMove through pages; the first four show while the document has more than one page
view:single-page, view:two-pages, view:cover-pagePages side by side
view:scroll-vertical, view:scroll-horizontal, view:scroll-pagedScroll down or across, or one page at a time
view:rotate-clockwise, view:rotate-counterclockwise, view:fullscreenTurn the view; fill the screen with the viewer, not your page: see Full screen
tool:pointer, tool:panSelect, or drag the pages
tool:zoomDrag 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:squigglyComment and mark up
tool:ink, tool:ink-highlight, tool:free-text, tool:callout, tool:insert-text, tool:replace-textDraw and write
tool:square, tool:circle, tool:line, tool:arrow, tool:polygon, tool:polylineShapes
tool:stamp, tool:image, tool:attachment, tool:signatureInsert
form:designSwitch 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-fieldBuild a form
tool:redact, redaction:applyMark areas, then remove them for good
tool:distance, tool:perimeter, tool:area, measurement:calibrateMeasure
mode:view, mode:annotate, mode:shapes, mode:insert, mode:form, mode:redact, mode:measureSwitch mode
panel:<id>Open or close a panel
search:findOpen search with the cursor in its field (Ctrl/Cmd+F), with a search panel in the layout
history:undo, history:redoUndo and redo
outline:add, outline:add-child, outline:rename, outline:set-destination, outline:properties, outline:deleteChange 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-itemUnfold or fold every outline item; open an outline row’s menu
attachment:add, attachment:save, attachment:go-to, attachment:delete, menu:attachment-itemAttach 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-itemSet 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:deleteAct on the selected pages: turn them, copy them in right after themselves, delete them
page:insert-blank, page:insert-fileInsert 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:pageSave the selected pages as a new PDF; select every page; open the selected pages’ menu
selection:copy, selection:highlight, selection:underline, selection:strikeoutAct on selected text
annotation:comment, annotation:style, annotation:link, annotation:group, annotation:ungroup, annotation:deleteAct on selected annotations
annotation:save-fileSave the file of a selected file attachment
help:shortcutsShow 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.

Was this page helpful?

Your feedback goes directly to the documentation team.