---
title: "Commands & shortcuts — Svelte"
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: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/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.svelte`**

```svelte
<script lang="ts">
  import { AnnotationToken } from '@embedpdf/svelte/annotation';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { DocumentsToken, Viewer } from '@embedpdf/svelte/runtime';
  import { engine } from './pdf';
  import { approveDocument } from './review';

  const plugins = [
    /* … */
    commandsPlugin({
      commands: [
        ...standardCommands,
        {
          id: 'review:approve',
          label: 'Approve',
          icon: 'check',
          shortcut: 'Mod+Enter',
          enabled: ({ get }) => get(AnnotationToken).canCreate(),
          run: ({ get }) => approveDocument(get(DocumentsToken)),
        },
      ],
    }),
  ];
</script>

<Viewer {engine} {plugins}>
  <!-- your toolbar and pages -->
</Viewer>
```

## Show a command

`useCommand(id)` gives you everything a button needs, and updates when any of it changes:

**`command-button.svelte`**

```svelte
<script lang="ts">
  import { useCommand } from '@embedpdf/svelte/commands';
  import Icon from './Icon.svelte';

  let { id }: { id: string } = $props();
  const command = useCommand(() => id);
</script>

{#if command.current.visible}
  {@const cmd = command.current}
  <button onclick={cmd.run} disabled={!cmd.enabled} aria-pressed={cmd.active}>
    <Icon name={cmd.icon} />
    {cmd.label}
    {#if cmd.shortcut}<kbd>{cmd.shortcut}</kbd>{/if}
  </button>
{/if}
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import CommandButton from './CommandButton.svelte';

  import '../buttons.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), commandsPlugin({ commands: standardCommands })];

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <div class="toolbar">
      <CommandButton id="page:previous" />
      <CommandButton id="page:next" />
      <CommandButton id="zoom:out" />
      <CommandButton id="zoom:in" />
      <CommandButton id="zoom:fit-width" />
      <CommandButton id="view:rotate-clockwise" />
      <CommandButton id="document:download" />
    </div>
    <Stage class="stage">
      <RenderLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

**`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);
}
```

**`CommandButton.svelte`**

```svelte
<!-- One button, drawn from its command: the label, the shortcut, and whether it can run now. -->
<script lang="ts">
  import { useCommand } from '@embedpdf/svelte/commands';

  let { id }: { id: string } = $props();

  const command = useCommand(() => id);
</script>

{#if command.current.visible}
  {@const cmd = command.current}
  <button
    type="button"
    class="button"
    title={cmd.label}
    disabled={!cmd.enabled}
    aria-pressed={cmd.active}
    onclick={cmd.run}
  >
    {cmd.label}
    {#if cmd.shortcut}<kbd class="shortcut">{cmd.shortcut}</kbd>{/if}
  </button>
{/if}
```

Pass the id as a function, and the button follows `id` when it changes.

`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

`useCommandShortcuts()` turns every command's shortcut into a working key. Call it once, near the top
of your viewer:

**`command-shortcuts.svelte`**

```svelte
<script lang="ts">
  import { useCommandShortcuts } from '@embedpdf/svelte/commands';

  useCommandShortcuts();
</script>

<!-- your toolbar and pages -->
```

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, pass `target`, a ref to the element
around one: its keys then work only while focus is inside it. `Mod` means Cmd on a Mac and Ctrl elsewhere. A help sheet lists
`listShortcuts()`:

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import ShortcutArea from './ShortcutArea.svelte';

  import '../shortcuts.css';

  const engine = localEngine();
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    commandsPlugin({ commands: standardCommands }),
  ];

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ShortcutArea />
  </DocumentGate>
</Viewer>
```

**`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);
}
```

**`KeyboardHelp.svelte`**

```svelte
<!-- The sheet lists every shortcut, and shows the last command a key ran. -->
<script lang="ts">
  import { useCommands, useCommandsEvent } from '@embedpdf/svelte/commands';
  import ShortcutRow from './ShortcutRow.svelte';

  const commands = useCommands();
  let last: string | null = $state(null);

  useCommandsEvent(
    (commands) => commands.onExecuted,
    ({ commandId }) => (last = commands.resolveCommand(commandId)?.label ?? commandId),
  );
</script>

<aside class="sheet">
  <p class="status">
    {last ? `Ran: ${last}` : 'Click the viewer, then press a key, such as → for the next page'}
  </p>
  <ul class="rows">
    {#each commands.listShortcuts() as { commandId, shortcut } (`${commandId} ${shortcut}`)}
      <ShortcutRow id={commandId} {shortcut} />
    {/each}
  </ul>
</aside>
```

**`ShortcutArea.svelte`**

```svelte
<!-- The keys work while focus is inside this viewer, so they leave the rest of the page alone. -->
<script lang="ts">
  import { RenderLayer } from '@embedpdf/svelte/render';
  import { Stage } from '@embedpdf/svelte/stage';
  import { useCommandShortcuts } from '@embedpdf/svelte/commands';
  import KeyboardHelp from './KeyboardHelp.svelte';

  let area: HTMLDivElement | undefined = $state();
  useCommandShortcuts({ target: () => area });
</script>

<!-- A click anywhere in the viewer gives it focus, so its keys work while it has it:
     `tabindex="-1"` makes it a place for keys, not a stop in the Tab order. -->
<div bind:this={area} class="layout" tabindex="-1">
  <KeyboardHelp />
  <Stage class="stage">
    <RenderLayer />
  </Stage>
</div>
```

**`ShortcutRow.svelte`**

```svelte
<!-- One row of the sheet: the key, and what it does. A command that can't run now is greyed out. -->
<script lang="ts">
  import { formatShortcut, useCommand } from '@embedpdf/svelte/commands';

  let { id, shortcut }: { id: string; shortcut: string } = $props();

  const command = useCommand(() => id);
</script>

{#if command.current.visible}
  <li class="row" data-disabled={!command.current.enabled || undefined}>
    <kbd class="key">{formatShortcut(shortcut)}</kbd>
    <span>{command.current.label}</span>
  </li>
{/if}
```

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

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import CommandPalette from './CommandPalette.svelte';

  import '../palette.css';

  const engine = localEngine();
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    commandsPlugin({ commands: standardCommands }),
  ];

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <div class="layout">
      <CommandPalette />
      <Stage class="stage">
        <RenderLayer />
      </Stage>
    </div>
  </DocumentGate>
</Viewer>
```

**`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);
}
```

**`CommandPalette.svelte`**

```svelte
<script lang="ts">
  import { useCommands } from '@embedpdf/svelte/commands';
  import Result from './Result.svelte';

  const commands = useCommands();
  let query = $state('page');
  const results = $derived(commands.searchCommands(query));

  // Enter runs the first result that can run now.
  function runFirst(event: KeyboardEvent) {
    const first = results.find((command) => commands.canExecute(command.id));
    if (event.key === 'Enter' && first) void commands.execute(first.id);
  }
</script>

<div class="palette">
  <input
    class="field"
    type="search"
    aria-label="Search commands"
    placeholder="Type a command…"
    bind:value={query}
    onkeydown={runFirst}
  />
  <ul class="results">
    {#each results as command (command.id)}
      <Result id={command.id} />
    {/each}
    {#if results.length === 0}
      <li class="empty">No command matches</li>
    {/if}
  </ul>
</div>
```

**`Result.svelte`**

```svelte
<!-- A result: its row follows the command, so it greys out the moment the command can't run. -->
<script lang="ts">
  import { useCommand } from '@embedpdf/svelte/commands';

  let { id }: { id: string } = $props();

  const command = useCommand(() => id);
</script>

{#if command.current}
  {@const cmd = command.current}
  <li>
    <button type="button" class="result" disabled={!cmd.enabled} onclick={cmd.run}>
      <span>{cmd.label}</span>
      {#if cmd.shortcut}<kbd class="shortcut">{cmd.shortcut}</kbd>{/if}
    </button>
  </li>
{/if}
```

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

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, StageToken, stagePlugin } from '@embedpdf/svelte/stage';
  import { commandsPlugin, standardCommands, type CommandDef } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import Toolbar from './Toolbar.svelte';

  import '../custom.css';

  const engine = localEngine();

  // 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 plugins = [
    stagePlugin(),
    renderPlugin(),
    commandsPlugin({ commands: [...standardCommands, twoPages] }),
  ];

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <Toolbar />
    <Stage class="stage">
      <RenderLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

**`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);
}
```

**`CategorySwitch.svelte`**

```svelte
<!-- Turning a category off hides its commands wherever they appear. -->
<script lang="ts">
  import { useCommands, useCommandsSettings } from '@embedpdf/svelte/commands';

  let { category, label }: { category: string; label: string } = $props();

  const commands = useCommands();
  const off = useCommandsSettings((settings) => settings.disabledCategories.includes(category));
</script>

<label class="switch">
  <input
    type="checkbox"
    checked={!off.current}
    onchange={(event) =>
      event.currentTarget.checked
        ? commands.enableCategory(category)
        : commands.disableCategory(category)}
  />
  {label}
</label>
```

**`CommandButton.svelte`**

```svelte
<script lang="ts">
  import { useCommand } from '@embedpdf/svelte/commands';

  let { id }: { id: string } = $props();

  const command = useCommand(() => id);
</script>

{#if command.current.visible}
  {@const cmd = command.current}
  <button
    type="button"
    class="button"
    disabled={!cmd.enabled}
    aria-pressed={cmd.active}
    onclick={cmd.run}
  >
    {cmd.label}
  </button>
{/if}
```

**`Toolbar.svelte`**

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { useCommands } from '@embedpdf/svelte/commands';
  import CategorySwitch from './CategorySwitch.svelte';
  import CommandButton from './CommandButton.svelte';

  const commands = useCommands();

  // Run from code: the document opens with its whole first page in view.
  onMount(() => {
    void commands.execute('zoom:fit-page');
  });
</script>

<div class="toolbar">
  <div class="buttons">
    <CommandButton id="zoom:out" />
    <CommandButton id="zoom:in" />
    <CommandButton id="zoom:fit-page" />
    <CommandButton id="layout:two-pages" />
  </div>
  <div class="switches">
    <CategorySwitch category="zoom" label="Zoom" />
    <CategorySwitch category="layout" label="Layout" />
  </div>
</div>
```

## Methods

Everything `useCommands()` 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 component, use `useCommand(id)`. [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 `useCommandsSettings()`. [Settings](#settings)                                                   |

Leave out `documentId`, and a command runs and resolves for the document in scope: the one a
[`<DocumentScope>`](../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
`useCommandsSettings()` 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.svelte`**

```svelte
<script lang="ts">
  import { useCommandsEvent } from '@embedpdf/svelte/commands';
  import { analytics } from './analytics';

  useCommandsEvent(
    (commands) => commands.onExecuted,
    ({ commandId }) => analytics.track('command', { commandId }),
  );
</script>

<!-- your toolbar and pages -->
```

| Event               | Fires when              | Carries                            |
| :------------------ | :---------------------- | :--------------------------------- |
| `onExecuted`        | A command ran           | `commandId`, `documentId`, `args`  |
| `onExecutionFailed` | A command's `run` threw | `commandId`, `documentId`, `error` |
| `onSettingsChanged` | 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.
