# Register custom commands

> Add application-owned actions to the same reactive command system as SuperDoc controls.



Register a custom command when an application action should share the Editor's enabled state, execution path, cleanup, and optional context-menu metadata. Keep ordinary application buttons outside the command system unless they need that integration.

Complete [Custom UI controller setup](/editor/custom-ui/controller-setup) first. This example expects `/contract.docx` to be available from your application.

## Add the application action [#add-the-application-action]

This control inserts a reusable clause at the live Editor selection:

```html
<label>
  Clause text
  <input id="clause-text" value="This agreement is governed by the laws of California." />
</label>
<button id="insert-clause" type="button" disabled>Insert clause</button>
<output id="command-status" aria-live="polite"></output>
<div id="editor" style="height: 70vh"></div>

<script type="module" src="/src/main.ts"></script>

```

## Register and execute the command [#register-and-execute-the-command]

Register after the Editor is ready and unregister during cleanup:

```ts
import { SuperDoc } from 'superdoc';
import type { CommandExecutionResult } from 'superdoc/ui';
import 'superdoc/style.css';

const clauseText = document.querySelector<HTMLInputElement>('#clause-text');
const insertClause = document.querySelector<HTMLButtonElement>('#insert-clause');
const status = document.querySelector<HTMLOutputElement>('#command-status');

if (!clauseText || !insertClause || !status) throw new Error('The custom command controls are incomplete.');

let disposeCommand: (() => void) | null = null;
let stopCommandState: (() => void) | null = null;
let removeHandlers: (() => void) | null = null;

const report = (result: CommandExecutionResult) => {
  if (result === false) {
    status.value = 'The clause could not be inserted.';
  } else if (typeof result === 'object' && !result.success) {
    status.value = result.failure.message;
  } else {
    status.value = 'Clause inserted.';
  }
};

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  onReady: ({ superdoc: readySuperDoc }) => {
    const ui = readySuperDoc.ui;
    const registration = ui.commands.register<{ text: string }>({
      id: 'insert-standard-clause',
      shortcut: 'Mod-Shift-K',
      getState: ({ state, documentMode }) => ({
        enabled: state.ready && documentMode !== 'viewing',
        disabled: !state.ready || documentMode === 'viewing',
        active: false,
        supported: true,
        reason: !state.ready ? 'not-ready' : documentMode === 'viewing' ? 'document-readonly' : undefined,
      }),
      execute: ({ payload, insertText }) => insertText(payload?.text ?? ''),
    });
    const command = registration.handle;

    const render = () => {
      const commandState = command.getState();
      insertClause.disabled = !commandState.enabled;
      insertClause.title = commandState.reason ?? '';
    };
    const run = async () => report(await command.executeAsync({ text: clauseText.value }));
    const runShortcut = (event: KeyboardEvent) => {
      if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === 'k') {
        event.preventDefault();
        void run();
      }
    };

    stopCommandState = command.observe(render);
    insertClause.addEventListener('click', run);
    window.addEventListener('keydown', runShortcut);

    removeHandlers = () => {
      insertClause.removeEventListener('click', run);
      window.removeEventListener('keydown', runShortcut);
    };
    disposeCommand = registration.unregister;
  },
});

window.addEventListener('beforeunload', () => {
  stopCommandState?.();
  removeHandlers?.();
  disposeCommand?.();
  superdoc.destroy();
});

```

The registration provides four optional integration points:

* `getState()` derives `enabled`, `active`, `value`, and `reason` from current Editor state.
* `execute()` receives the typed payload, live selection, document mode, controller, and Document API facade.
* `shortcut` describes the intended shortcut. Your application still owns the keyboard listener and conflict policy.
* `contextMenu` describes an item that your menu can obtain with `ui.commands.getContextMenuItems(context)`.

Use the returned handle to observe state and call `executeAsync()`. This keeps the button and keyboard path on one execution contract. Inspect the result before reporting success or starting dependent work.

## Keep commands focused [#keep-commands-focused]

A custom command should represent one user intent. Compose existing commands through `context.executeAsync()`, use `context.insertText()` for the narrow insertion helper, or call the public `context.doc` Document API for document operations. Do not reach into Editor state, view objects, or DOM offsets.

Custom commands do not create an authorization boundary. The browser can gate normal interactions, but your backend must still enforce document access, identity, persistence, and collaboration permissions.
