---
title: "Buttons & commands — Angular"
description: "Add your own buttons, change what a built-in button does, give actions keyboard shortcuts and icons, and run any action from code."
integration: "Angular"
source: "https://www.cloudpdf.com/docs/viewer/angular/customize/commands"
---

# 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](layout):

```js
{
  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:

**`approve.ts`**

```typescript
import { Component, signal, ViewEncapsulation } from '@angular/core';
import { EpdfPdfViewer, type ViewerConfig } from '@cloudpdf/viewer-angular';

const welcome = { grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };

@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' }),
  };
}
```

**`approve.css`**

```css
/* Colors, light and dark, as the viewer follows the system's */
:root {
  --ink: #111827;
  --muted: #6b7280;
  --line: #d1d5db;
  --tint: #f3f4f6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #f9fafb;
    --muted: #9ca3af;
    --line: #4b5563;
    --tint: #111827;
  }
}

/* The app: your status line above the viewer */
.app {
  display: flex;
  flex-direction: column;
}

.status {
  margin: 0;
  padding: 8px 14px;
  font:
    13px/1.4 system-ui,
    sans-serif;
  color: var(--muted);
  background: var(--tint);
  border-bottom: 1px solid var(--line);
}

.status strong {
  color: var(--ink);
}

.viewer {
  flex: 1;
  min-height: 0;
}
```

> **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

| 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](translations) label                                                               |
| `icon`                 | An icon's name: the viewer's or [your own](#icons)                                                                                  |
| `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](menus) it opens, instead of `run`                                                                                          |
| `panel`                | A [panel](panels) it opens and closes, instead of `run`                                                                             |
| `requires`             | The [permissions](#permissions) it needs from the scope. Without them, it's hidden.                                                 |
| `categories`           | The features it belongs to, so [`disable`](../features/turning-off) 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](https://www.cloudpdf.com/docs/headless/angular/ui/commands#define-a-command).

## 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:

```js
{
  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`**

```typescript
import { Component, computed, signal } from '@angular/core';
import { EpdfPdfViewer, type ViewerConfig } from '@cloudpdf/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]="{ grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' }"
      [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:

```js
{
  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](https://tabler.io/icons) and
[Lucide](https://lucide.dev) icons paste in as they are. Give a name to each of yours:

```js
{
  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:

```js
{
  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](https://www.cloudpdf.com/docs/headless/angular/concepts/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](../features/turning-off)
it instead: `disable: ['acme:approve']`.

A command whose setting a plugin [locks](https://www.cloudpdf.com/docs/headless/angular/viewing/stage#lock-a-setting) 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

```js
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](menus)                                                                                                                                                           |
| `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](https://www.cloudpdf.com/docs/headless/angular/viewing/stage#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](../concepts/how-it-works#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:snapshot`                                                                                                                                                                   | Drag a box to [take that part of the page](../documents#take-a-snapshot) as a picture, or click for the whole page                                                                      |
| `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](panels)                                                                                                                                                         |
| `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](menus)                                                                                                                  |
| `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](menus)                                                  |
| `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](menus)                                                                   |
| `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](menus)                                                                                         |
| `selection:copy`, `selection:highlight`, `selection:underline`, `selection:strikeout`                                                                                             | Act on selected text                                                                                                                                                                    |
| `snapshot:copy`, `snapshot:save`, `snapshot:clear`                                                                                                                                | Copy the snapshot (`Ctrl`/`Cmd`+`C`), save it as a PNG, or let it go (`Escape`)                                                                                                         |
| `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](https://www.cloudpdf.com/docs/headless/angular/annotations/tools) 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](theming#every-color).

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.
