---
title: "Commands & shortcuts — Angular"
description: "Give every action in your viewer one name, with its label, icon, state and keyboard shortcut, and run it from buttons, menus and keys."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/ui/commands"
---

# Commands & shortcuts

A **command** is one action of your viewer, like "zoom in" or "download", defined once: its label, its
icon, when it's enabled or active, what it does, and its keyboard shortcut. Toolbars, menus, a command
palette and the keyboard all run the same command, so they always agree. Register the plugin with the
commands you need:

**`commands-plugin.ts`**

```typescript
import { Component } from '@angular/core';
import { AnnotationToken } from '@embedpdf/angular/annotation';
import { standardCommands, withCommands } from '@embedpdf/angular/commands';
import { DocumentsToken, provideEmbedPdf } from '@embedpdf/angular/runtime';
import { engine } from './pdf';
import { approveDocument } from './review';

@Component({
  selector: 'app-document-viewer',
  providers: [
    provideEmbedPdf(
      { engine },
      /* … */
      withCommands({
        commands: [
          ...standardCommands,
          {
            id: 'review:approve',
            label: 'Approve',
            icon: 'check',
            shortcut: 'Mod+Enter',
            enabled: ({ get }) => get(AnnotationToken).canCreate(),
            run: ({ get }) => approveDocument(get(DocumentsToken)),
          },
        ],
      }),
    ),
  ],
  template: `<!-- your toolbar and pages -->`,
})
export class DocumentViewer {}
```

## Show a command

The `epdfCommand` directive runs the command on click and keeps your button's `disabled`, `aria-pressed`, `title` and `hidden` in step with it. Its template reference gives you the rest, as signals:

**`command-button.ts`**

```typescript
import { Component, input } from '@angular/core';
import { EpdfCommand } from '@embedpdf/angular/commands';
import { Icon } from './icon';

@Component({
  selector: 'app-command-button',
  imports: [EpdfCommand, Icon],
  template: `
    <button [epdfCommand]="id()" #command="epdfCommand">
      <app-icon [name]="command.icon()" />
      {{ command.label() }}
      @if (command.shortcut()) {
        <kbd>{{ command.shortcut() }}</kbd>
      }
    </button>
  `,
})
export class CommandButton {
  readonly id = input.required<string>();
}
```

**`buttons.ts`**

```typescript
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfCommand, standardCommands, withCommands } from '@embedpdf/angular/commands';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfCommand],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withCommands({ commands: standardCommands }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './buttons.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        <!--
          One button per command: epdfCommand runs it, and keeps disabled, aria-pressed and the
          title in step; the label and the shortcut come from its template reference.
        -->
        @for (id of commands; track id) {
          <button type="button" class="button" [epdfCommand]="id" #command="epdfCommand">
            {{ command.label() }}
            @if (command.shortcut(); as shortcut) {
              <kbd class="shortcut">{{ shortcut }}</kbd>
            }
          </button>
        }
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly commands = [
    'page:previous',
    'page:next',
    'zoom:out',
    'zoom:in',
    'zoom:fit-width',
    'view:rotate-clockwise',
    'document:download',
  ];
}
```

**`buttons.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The buttons, above the pages; they wrap on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 10px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* A command that is active, such as the current fit, shows as pressed */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.shortcut {
  font-family: inherit;
  font-size: 11px;
  font-weight: 500;
  color: var(--muted);
  padding: 2px 5px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--canvas);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

For UI that isn't a button, such as a row in a shortcuts list, `commands.commandOf(id)` gives the
same command as a signal.

`shortcut` is formatted for the user's platform: `⌘K` on a Mac, `Ctrl+K` elsewhere. The
[toolbar](toolbar) draws commands the same way.

There's always a command, so a button never checks for a missing one: an id no plugin registered
comes back labelled with its id, neither visible nor enabled, the way a turned-off command does,
and in development it warns once, with the closest id there is. Checking `visible` covers a
command that's turned off, not allowed or missing. `resolveCommand(id)` is the lookup for code,
and says `null` for an id it doesn't have.

## The standard commands

`standardCommands` covers what every viewer has, with labels in [every language you load](translations):

| Command                                                 | Does                                          | Shortcut                |
| :------------------------------------------------------ | :-------------------------------------------- | :---------------------- |
| `zoom:in`, `zoom:out`                                   | Zoom in or out                                | `Mod+=`, `Mod+-`        |
| `zoom:fit-width`, `zoom:fit-page`                       | Fit the page's width, or the whole page       |                         |
| `page:next`, `page:previous`, `page:first`, `page:last` | Move through pages                            | `→`, `←`, `Home`, `End` |
| `view:rotate-clockwise`, `view:rotate-counterclockwise` | Turn the view                                 |                         |
| `tool:pointer`, `tool:pan`                              | Switch tools                                  | `V`, `H`                |
| `tool:<id>`                                             | Any registered tool, such as `tool:highlight` |                         |
| `selection:copy`                                        | Copy the selected text                        | `Mod+C`                 |
| `annotation:delete`                                     | Delete the selected annotations               | `Delete`                |
| `document:download`, `document:print`                   | Download or print the document                | `Mod+S`, `Mod+P`        |

Each one is enabled only when it can run: `selection:copy` needs a selection and permission to copy,
`annotation:delete` a selection the user may delete.

## Keyboard shortcuts

`withCommandShortcuts()` turns every command's shortcut into a working key. Add it to `provideEmbedPdf()`, next to `withCommands()`:

**`command-shortcuts.ts`**

```typescript
import { Component } from '@angular/core';
import { standardCommands, withCommandShortcuts, withCommands } from '@embedpdf/angular/commands';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { engine } from './pdf';

@Component({
  selector: 'app-document-viewer',
  providers: [
    provideEmbedPdf(
      { engine },
      /* … */
      withCommands({ commands: standardCommands }),
      withCommandShortcuts(),
    ),
  ],
  template: `<!-- your toolbar and pages -->`,
})
export class DocumentViewer {}
```

It ignores keys typed into text fields, except a command's whose `shortcutInTextFields` is `true`
(a Find that should work from the search box too), runs commands for the active document, and leaves a key alone
when its command can't run right now. A command that's [turned off](#turn-features-off) or [not
allowed](#commands-and-permissions) still takes its key, so the browser doesn't do something else with
it (`Mod+P` printing the page), and `onRefused(commandId, refusal)` tells you why: `refusal.message`
is a sentence to show. With more than one viewer on a page, leave
`withCommandShortcuts()` out and put the `epdfCommandShortcuts` directive on the element around one
(`<div tabindex="0" epdfCommandShortcuts>`): its keys then work only while focus is inside it. `Mod` means Cmd on a Mac and Ctrl elsewhere. A help sheet lists
`listShortcuts()`:

**`shortcuts.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import {
  EpdfCommands,
  EpdfCommandShortcuts,
  formatShortcut,
  standardCommands,
  withCommands,
} from '@embedpdf/angular/commands';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// One row of the sheet: the key, and what it does. A command that can't run now is greyed out.
@Component({
  selector: 'li[demoShortcutRow]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'row',
    '[attr.data-disabled]': "command().enabled ? null : ''",
    '[style.display]': "command().visible ? null : 'none'",
  },
  template: `<kbd class="key">{{ key() }}</kbd><span>{{ command().label }}</span>`,
})
export class ShortcutRow {
  readonly commandId = input.required<string>();
  readonly shortcut = input.required<string>();

  protected readonly command = inject(EpdfCommands).commandOf(() => this.commandId());
  protected readonly key = computed(() => formatShortcut(this.shortcut()));
}

// The sheet lists every shortcut, and shows the last command a key ran.
@Component({
  selector: 'aside[demoKeyboardHelp]',
  imports: [ShortcutRow],
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'sheet' },
  template: `
    <p class="status">
      {{ last() ? 'Ran: ' + last() : 'Click the viewer, then press a key, such as → for the next page' }}
    </p>
    <ul class="rows">
      @for (row of shortcuts; track row.commandId + ' ' + row.shortcut) {
        <li demoShortcutRow [commandId]="row.commandId" [shortcut]="row.shortcut"></li>
      }
    </ul>
  `,
})
export class KeyboardHelp {
  private readonly commands = inject(EpdfCommands);
  protected readonly shortcuts = this.commands.listShortcuts();
  protected readonly last = signal<string | null>(null);

  constructor() {
    this.commands.executed$
      .pipe(takeUntilDestroyed())
      .subscribe(({ commandId }) =>
        this.last.set(this.commands.resolveCommand(commandId)?.label ?? commandId),
      );
  }
}

// The keys work while focus is inside this viewer, so they leave the rest of the page alone. A
// click anywhere in it gives it focus: `tabindex="-1"` makes it a place for keys, not a stop in the
// Tab order.
@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfCommandShortcuts,
    KeyboardHelp,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withCommands({ commands: standardCommands }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './shortcuts.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <div
      *epdfDocumentGate="let document; fallback: loading"
      class="layout"
      tabindex="-1"
      epdfCommandShortcuts
    >
      <aside demoKeyboardHelp></aside>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </div>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`shortcuts.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The sheet beside the pages, and above them on a phone */
.layout {
  container-type: inline-size;
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
}

/* The keys work while focus is inside the viewer: show where that is, for the keyboard */
.layout:focus-visible,
.layout:has(:focus-visible) {
  outline: 2px solid var(--focus, #7db6ff);
  outline-offset: 4px;
  border-radius: 14px;
}

@container (min-width: 620px) {
  .layout {
    grid-template-columns: 220px 1fr;
  }
}

.sheet {
  box-sizing: border-box;
  max-height: 420px;
  overflow: auto;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 1px 2px var(--shadow);
}

.status {
  margin: 0 0 10px;
  padding: 8px 10px;
  font-size: 12px;
  font-weight: 550;
  color: var(--accent);
  background: var(--tint);
  border-radius: 8px;
}

.rows {
  margin: 0;
  padding: 0;
  list-style: none;
}

.row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 2px;
  font-size: 13px;
  color: var(--ink);
}

.row[data-disabled] {
  color: var(--muted);
  opacity: 0.6;
}

.key {
  min-width: 52px;
  font-family: inherit;
  font-size: 11px;
  font-weight: 600;
  text-align: center;
  color: var(--ink);
  padding: 3px 6px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 6px;
  background: var(--canvas);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Run a command from code

```ts
await commands.execute('zoom:in');
await commands.execute('review:approve', { args: { note: 'Looks good' } });
```

`execute()` resolves with `{ status: 'executed' }`, or `{ status: 'rejected', reason }` when the command
is disabled, hidden or unknown.

## A command palette

`searchCommands()` finds commands by their label, for a palette like the one in code editors:

```ts
const results = commands.searchCommands(query); // visible commands whose label or id matches
```

A disabled command is still in the results, so show it greyed out.

**`palette.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import {
  EpdfCommand,
  EpdfCommands,
  standardCommands,
  withCommands,
} from '@embedpdf/angular/commands';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

@Component({
  selector: 'div[demoCommandPalette]',
  imports: [EpdfCommand],
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'palette' },
  template: `
    <input
      #field
      class="field"
      type="search"
      aria-label="Search commands"
      placeholder="Type a command…"
      [value]="query()"
      (input)="query.set(field.value)"
      (keydown.enter)="runFirst()"
    />
    <ul class="results">
      <!-- A result follows its command, so it greys out the moment the command can't run. -->
      @for (result of results(); track result.id) {
        <li>
          <button type="button" class="result" [epdfCommand]="result.id" #command="epdfCommand">
            <span>{{ command.label() }}</span>
            @if (command.shortcut(); as shortcut) {
              <kbd class="shortcut">{{ shortcut }}</kbd>
            }
          </button>
        </li>
      } @empty {
        <li class="empty">No command matches</li>
      }
    </ul>
  `,
})
export class CommandPalette {
  private readonly commands = inject(EpdfCommands);
  protected readonly query = signal('page');
  protected readonly results = computed(() => this.commands.searchCommands(this.query()));

  // Enter runs the first result that can run now.
  protected runFirst() {
    const first = this.results().find((command) => this.commands.canExecute(command.id));
    if (first) void this.commands.execute(first.id);
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, CommandPalette],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withCommands({ commands: standardCommands }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './palette.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <div *epdfDocumentGate="let document; fallback: loading" class="layout">
      <div demoCommandPalette></div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </div>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`palette.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The palette beside the pages, and above them on a phone */
.layout {
  container-type: inline-size;
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
}

@container (min-width: 620px) {
  .layout {
    grid-template-columns: 240px 1fr;
  }
}

.palette {
  box-sizing: border-box;
  max-height: 420px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 1px 2px var(--shadow);
}

.field {
  appearance: none;
  box-sizing: border-box;
  width: 100%;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.results {
  margin: 0;
  padding: 0;
  list-style: none;
  overflow: auto;
}

.result {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font: inherit;
  font-size: 13px;
  text-align: start;
  color: var(--ink);
  background: transparent;
  border: none;
  border-radius: 8px;
  padding: 7px 8px;
  cursor: pointer;
}

.result:hover,
.result:focus-visible {
  outline: none;
  background: var(--tint);
}

/* A command that can't run now stays in the list, greyed out */
.result:disabled {
  color: var(--muted);
  opacity: 0.55;
  cursor: not-allowed;
  background: transparent;
}

.shortcut {
  font-family: inherit;
  font-size: 11px;
  color: var(--muted);
  padding: 2px 5px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--canvas);
}

.empty {
  padding: 8px;
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Define a command

| Field                    | What it is                                                                                                               |
| :----------------------- | :----------------------------------------------------------------------------------------------------------------------- |
| `id`                     | A unique name, by convention `area:action`                                                                               |
| `label`                  | What the user sees, or `labelKey` for a [translated](translations) label                                                 |
| `icon`                   | Your icon's name; your buttons turn it into an icon                                                                      |
| `shortcut`               | A key such as `'Mod+K'`, or a list of them                                                                               |
| `shortcutInTextFields`   | `true`: its shortcut runs while the cursor is in a text field too                                                        |
| `enabled`                | When it can run, as a function of the viewer's state                                                                     |
| `active`                 | When it shows as pressed, such as the current tool                                                                       |
| `visible`                | When it shows at all                                                                                                     |
| `run`                    | What it does                                                                                                             |
| `requires`               | The [permissions](../concepts/permissions) it needs, such as `'doc.annotate.modify'`. Without them, it's hidden.         |
| `categories`             | The features it belongs to, so you can [turn them off](#turn-features-off) together, such as `['annotations.highlight']` |
| `panel`, `menu`, `modal` | A command that opens a [panel, menu or dialog](panels) needs no `run`                                                    |

`enabled`, `active` and `visible` get the viewer's state through `get(Token)`, and are asked again
whenever anything changes, so a button is always right without events or effects of your own.

## Turn features off

Put commands in categories, and turn a whole category off: its commands are hidden and refused,
wherever they appear. A category turns off every category under it, on whole dotted parts:
`annotations` also turns off `annotations.highlight`. The `disabledCategories` [setting](#settings)
turns categories off from the start, and while the app runs:

```ts
commands.disableCategory('annotations');
commands.enableCategory('annotations');
```

Turn off one command the same way, with `disableCommand(id)` and the `disabledCommands` setting. A
command that's turned off is refused by `execute()` too, and its shortcut does nothing.

A command of your own, a category switch, and a command run from code when the document opens:

**`custom.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, StageToken, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import {
  EpdfCommand,
  EpdfCommands,
  standardCommands,
  withCommands,
} from '@embedpdf/angular/commands';
import type { CommandDef } from '@embedpdf/angular/commands';
import { localEngine } from '@embedpdf/engine';

// A command of your own: pressed while the pages show two at a time.
const twoPages: CommandDef = {
  id: 'layout:two-pages',
  label: 'Two pages',
  categories: ['layout'],
  active: ({ get }) => get(StageToken).getSettings().spread === 'odd',
  run: ({ get }) => {
    const stage = get(StageToken);
    stage.updateSettings({ spread: stage.getSettings().spread === 'odd' ? 'none' : 'odd' });
  },
};

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Turning a category off hides its commands wherever they appear.
@Component({
  selector: 'label[demoCategorySwitch]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'switch' },
  template: `
    <input #box type="checkbox" [checked]="!off()" (change)="toggle(box.checked)" />
    <ng-content />
  `,
})
export class CategorySwitch {
  readonly category = input.required<string>({ alias: 'demoCategorySwitch' });

  private readonly commands = inject(EpdfCommands);
  protected readonly off = computed(() =>
    this.commands.settings().disabledCategories.includes(this.category()),
  );

  protected toggle(on: boolean) {
    if (on) this.commands.enableCategory(this.category());
    else this.commands.disableCategory(this.category());
  }
}

@Component({
  selector: 'div[demoToolbar]',
  imports: [EpdfCommand, CategorySwitch],
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'toolbar' },
  template: `
    <div class="buttons">
      @for (id of buttons; track id) {
        <button type="button" class="button" [epdfCommand]="id" #command="epdfCommand">
          {{ command.label() }}
        </button>
      }
    </div>
    <div class="switches">
      <label demoCategorySwitch="zoom">Zoom</label>
      <label demoCategorySwitch="layout">Layout</label>
    </div>
  `,
})
export class Toolbar {
  protected readonly buttons = ['zoom:out', 'zoom:in', 'zoom:fit-page', 'layout:two-pages'];

  constructor() {
    // Run from code: the document opens with its whole first page in view.
    void inject(EpdfCommands).execute('zoom:fit-page');
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, Toolbar],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withCommands({ commands: [...standardCommands, twoPages] }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './custom.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div demoToolbar></div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`custom.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The buttons and the switches, above the pages; they wrap on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 14px;
}

.buttons,
.switches {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 10px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink);
  padding: 6px 10px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}

.switch input {
  margin: 0;
  accent-color: var(--accent);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Methods

Everything `inject(EpdfCommands)` gives you.

| Method                                                                               | What it does                                                                                                                                                                                                      |
| :----------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `execute(id, { args?, documentId? })`                                                | Runs a command. Resolves `{ status, reason? }`: `'executed'`, or `'rejected'` with why. A `run` that throws rejects. [Run a command from code](#run-a-command-from-code)                                          |
| `canExecute(id, documentId?)`                                                        | Whether a command can run now: it's registered, enabled and visible                                                                                                                                               |
| `resolveCommand(id, documentId?)`                                                    | A command as a button shows it: label, icon, shortcuts, and whether it's enabled, active and visible. `null` for an unknown id. In a template, use the `epdfCommand` directive. [Show a command](#show-a-command) |
| `getRefusal(id, documentId?)`                                                        | Why a command won't run whatever the moment, as `{ reason, message }`: `'turned-off'` or `'not-allowed'`, with a sentence for the user. `null` when it may run, or can't only right now, and for an unknown id    |
| `listCommands(documentId?)`                                                          | Every command, resolved the same way, in the order they were added                                                                                                                                                |
| `searchCommands(query, documentId?)`                                                 | The visible commands whose label or id contains the query. [A command palette](#a-command-palette)                                                                                                                |
| `listShortcuts()`                                                                    | Every shortcut with its command, as `{ commandId, shortcut }`, for a keyboard help sheet. [Keyboard shortcuts](#keyboard-shortcuts)                                                                               |
| `registerCommand(command, { replace? })`, `registerCommands(commands, { replace? })` | Adds one command, or several, while the app runs, and returns a function that removes them. An id that's taken is refused unless you pass `replace: true`. [Define a command](#define-a-command)                  |
| `getCommand(id)`, `hasCommand(id)`                                                   | A command as you defined it, or `null`; whether it's registered                                                                                                                                                   |
| `listCommandIds()`                                                                   | Every command's id, in the order they were added                                                                                                                                                                  |
| `disableCategory(category)`, `enableCategory(category)`                              | Turns a category off, with every category under it, or on again. [Turn features off](#turn-features-off)                                                                                                          |
| `setDisabledCategories(categories)`                                                  | Replaces the list of categories that are off                                                                                                                                                                      |
| `getDisabledCategories()`, `isCategoryDisabled(category)`                            | The categories that are off; whether one is, itself or through a category it's under                                                                                                                              |
| `disableCommand(id)`, `enableCommand(id)`                                            | Turns one command off, or on again. [Turn features off](#turn-features-off)                                                                                                                                       |
| `setDisabledCommands(ids)`                                                           | Replaces the list of commands that are off one by one                                                                                                                                                             |
| `getDisabledCommands()`, `isCommandDisabled(id)`                                     | The commands that are off one by one; whether a command is off, by its id or through one of its categories                                                                                                        |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()`                        | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `inject(EpdfCommands).settings()`. [Settings](#settings)                                            |

Leave out `documentId`, and a command runs and resolves for the document in scope: the one a
[`[epdfDocumentScope]`](../documents/tabs#talk-to-a-specific-document) around your component names, or
else the active one. Every method that returns a promise also takes a `signal` in its options, to
cancel it.

## Settings

Set them when you register the plugin, and change them while the app runs with
`commands.updateSettings({ … })`; `commands.resetSettings()` goes back to what you registered, and
`inject(EpdfCommands).settings()` gives you the current settings.

| Setting              | What it does                                                                                                             | Default  |
| :------------------- | :----------------------------------------------------------------------------------------------------------------------- | :------- |
| `commands`           | The commands your viewer has                                                                                             | none     |
| `disabledCategories` | Categories that start turned off, with every category under them                                                         | none     |
| `disabledCommands`   | Commands that start turned off, by id                                                                                    | none     |
| `notAllowed`         | What a command the scope doesn't allow does: `'hide'` leaves it out, `'disable'` shows it disabled with `disabledReason` | `'hide'` |

## Events

**`command-events.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfCommands } from '@embedpdf/angular/commands';
import { analytics } from './analytics';

@Component({
  selector: 'app-viewer-layout',
  template: `<!-- your toolbar and pages -->`,
})
export class ViewerLayout {
  private readonly commands = inject(EpdfCommands);

  constructor() {
    this.commands.executed$
      .pipe(takeUntilDestroyed())
      .subscribe(({ commandId }) => analytics.track('command', { commandId }));
  }
}
```

| Event              | Fires when              | Carries                            |
| :----------------- | :---------------------- | :--------------------------------- |
| `executed$`        | A command ran           | `commandId`, `documentId`, `args`  |
| `executionFailed$` | A command's `run` threw | `commandId`, `documentId`, `error` |
| `settingsChanged$` | The settings changed    | `settings`, `changed`              |

## Good to know

### Commands and permissions

A command that needs a permission says so with `requires: ['doc.annotate.modify']`. When the user
doesn't have it, the command isn't visible, so its buttons and menu rows are gone, and its shortcut
does nothing. With the `notAllowed: 'disable'` setting it shows disabled instead, and the resolved
command's `disabledReason` says why, for its tooltip: the i18n string for
`commands.notAllowed.<permission>` (dots and colons as dashes, such as
`commands.notAllowed.doc-print`), else `commands.notAllowed.default`. Keep `enabled` for what can't
run right now, such as **Copy** with nothing selected. The standard commands already say what they
need.
