---
title: "Buttons & commands — React"
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: "React"
source: "https://www.embedpdf.com/docs/viewer/react/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.tsx`**

```tsx
import { useState } from 'react';
import { PDFViewer, type ViewerConfig } from '@embedpdf/viewer-react';
import './approve.css';

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

// A constant: the command calls your app through `appState`, so it never holds an old value.
const config: ViewerConfig<{ approve(): void }> = {
  commands: [
    {
      id: 'acme:approve',
      label: 'Approve',
      icon: 'check',
      shortcut: 'Mod+Enter',
      run: ({ appState }) => appState.approve(),
    },
  ],
  layout: (layout) => layout.add('acme:approve', { to: 'main', section: 'end' }),
};

export default function App() {
  const [status, setStatus] = useState('Waiting for approval');
  const approve = () => setStatus(`Approved at ${new Date().toLocaleTimeString()}`);

  return (
    <div className="app" style={{ height: '100%' }}>
      <p className="status">
        Status: <strong>{status}</strong>
      </p>
      <PDFViewer src={welcome} {...config} appState={{ approve }} className="viewer" />
    </div>
  );
}
```

**`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.embedpdf.com/docs/headless/react/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.tsx`**

```tsx
import { useState } from 'react';
import { PDFViewer, type ViewerConfig } from '@embedpdf/viewer-react';

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

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

// A constant: the commands read your state from `appState`, so nothing goes stale.
const config: ViewerConfig<Review> = {
  commands: [
    {
      id: 'acme:approve',
      label: 'Approve',
      icon: 'check',
      enabled: ({ appState }) => appState.done.length === CHECKS.length,
      active: ({ appState }) => appState.status === 'approved',
      run: ({ appState }) => appState.approve(),
    },
  ],
  layout: (layout) => layout.add('acme:approve', { to: 'main', section: 'end' }),
};

export function ReviewApp() {
  const [done, setDone] = useState<string[]>([]);
  const [status, setStatus] = useState<Review['status']>('review');

  const toggle = (check: string) =>
    setDone((done) => (done.includes(check) ? done.filter((c) => c !== check) : [...done, check]));

  return (
    <>
      <fieldset>
        {CHECKS.map((check) => (
          <label key={check}>
            <input type="checkbox" checked={done.includes(check)} onChange={() => toggle(check)} />
            {check}
          </label>
        ))}
      </fieldset>
      <PDFViewer
        src="/report.pdf"
        {...config}
        appState={{ done, status, approve: () => setStatus('approved') }}
        style={{ height: '100vh' }}
      />
    </>
  );
}
```

Because the commands read your state from `appState`, never from a variable they closed over, they
can be a constant outside your component and never see an old value. Give it a new value when
something changes, as you do with any prop: the viewer sees a new object, not a change inside the
one it has (`[...done, item]`, not `done.push(item)`). In TypeScript, `ViewerConfig<AppState>` gives
`appState` its type in every command.

## Change what a built-in button does

A command with a built-in id replaces the built-in one, everywhere it appears: the button, the
menu row and the shortcut. This one makes **Download** save to your server instead:

```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.embedpdf.com/docs/headless/react/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.embedpdf.com/docs/headless/react/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.embedpdf.com/docs/headless/react/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: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                                                                                                                                                                    |
| `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.embedpdf.com/docs/headless/react/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.
