# Custom UI controller setup

> Bind a custom control to live SuperDoc command state and execute it safely.



This guide creates one application-owned Bold button over the public `superdoc/ui` controller. The button follows live selection state, executes the real Editor command, and cleans up with the Editor.

Complete the [Editor quickstart](/editor/quickstart) first if you have not mounted a v2 Editor before.

## Add the HTML surface [#add-the-html-surface]

Create the button and an editor container:

```html
<button id="bold" type="button" disabled aria-pressed="false">Bold</button>
<div id="editor"></div>

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

```

Copy the [tracked-changes fixture](/fixtures/tracked-changes.docx) to your app's public directory as `contract.docx`, or update the example to use another DOCX URL.

## Bind the controller after readiness [#bind-the-controller-after-readiness]

Use this as `src/main.ts`:

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

const boldButton = document.querySelector<HTMLButtonElement>('#bold');
if (!boldButton) throw new Error('The Bold button is missing.');

let stopObserving: (() => void) | null = null;
let removeClickHandler: (() => void) | null = null;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  // This page's Bold button replaces one toolbar control, so the built-in
  // toolbar is the only surface turned off.
  ui: { toolbar: false },
  onReady: ({ superdoc: readySuperDoc }) => {
    const bold = readySuperDoc.ui.commands.get('bold');

    const render = (state: ReturnType<typeof bold.getState>) => {
      boldButton.disabled = !state.enabled;
      boldButton.setAttribute('aria-pressed', String(state.active));
      boldButton.title = state.reason ?? 'Toggle bold';
    };

    const onBoldClick = async () => {
      const result = await bold.executeAsync();
      if (result === false) console.warn('Bold is not available for the current selection.');
    };

    render(bold.getState());
    stopObserving = bold.observe(render);
    boldButton.addEventListener('click', onBoldClick);
    removeClickHandler = () => boldButton.removeEventListener('click', onBoldClick);
  },
});

window.addEventListener('beforeunload', () => {
  stopObserving?.();
  removeClickHandler?.();
  // Tears the controller down too. Never call `superdoc.ui.destroy()` yourself.
  superdoc.destroy();
});

```

Every SuperDoc instance owns one controller at `superdoc.ui`. Read it as many times as you like; you always get the same object, and so does the built-in toolbar if you render one. Reading it before the document is ready is safe, but the command handle only reports real state once the Editor is ready, which is why the binding happens in `onReady`.

The instance owns the controller's lifecycle. `superdoc.destroy()` tears it down, and `superdoc.ui` is typed as the borrowed handle `BorrowedSuperDocUI`, which has no `destroy()` at all. Calling it is a compile error rather than a rule to remember, because doing so would freeze command state for every other part of your interface still reading it.

> **When you need a separate controller (note)**
>
> `createSuperDocUI()` from `superdoc/ui` builds an independent controller that you own and must destroy. It exists for
> cases like driving a detached preview from its own state. Application UI should read `superdoc.ui` instead.


## Remove the built-in surfaces [#remove-the-built-in-surfaces]

This guide's Bold button replaces one toolbar control, so `ui: { toolbar: false }` is all it needs. As your interface takes over more of the document experience, each built-in surface has its own switch:

* `ui: { toolbar: false }` renders no built-in toolbar.
* `ui: { comments: false }` removes the built-in comments interface: the sidebar, floating threads, and comment dialog.
* `ui: { contextMenu: false }` removes the built-in right-click and slash menus.
* `ui: { linkPopover: false }` suppresses the popover that opens when a reader clicks a link.

Take only the switches whose surface you actually replace. They are independent, and each removes one presentation and nothing else, so turning off a surface you have not rebuilt leaves the reader with less than SuperDoc would have given them. The [custom comments UI](/editor/custom-ui/comments) shows the comments switch used the right way round: it mounts with `ui: { comments: false }` because it renders its own panel, and leaves everything else in place.

When your application owns the entire interface, `ui: false` turns off every built-in surface at once. It does not reduce what the editor can do: editing, the Document API, `interaction`, `surfaces`, and `superdoc.ui` all stay exactly as they were.

These are rendering switches, not permissions. `ui: { comments: false }` hides the built-in comments interface but does not stop your own panel from creating comments; `interaction: { comments: { readOnly: true } }` is what refuses the write.

The underlying capabilities stay available through `superdoc.ui`. Comment threads in the DOCX are still parsed and readable through `superdoc.ui.comments` with the surface off, and `resolve` and `reopen` still commit. See [built-in comments](/editor/built-in-ui/comments) for the full distinction.

## Follow command state [#follow-command-state]

`getState()` provides the initial state. `observe()` keeps the button synchronized as the selection and document change:

* `enabled` controls whether the action can run.
* `active` indicates whether the current selection is bold.
* `reason` explains why a known command is disabled.

This is more reliable than deriving state from rendered DOM nodes. The document canvas is presentation; the controller is the public UI state boundary.

## Execute and inspect the result [#execute-and-inspect-the-result]

The click handler calls `executeAsync()`. A `false` result means the command did not run. A mutation receipt should be inspected before starting dependent work.

For a complete toolbar, repeat this pattern with command IDs exposed by `superdoc/ui`, but keep the first implementation focused on controls the workflow actually needs.

> **Verification target (success)**
>
> Select text in the DOCX. The Bold button should become enabled, reflect the active formatting state, and toggle bold
> without reading or changing the document DOM directly.


Build the next control with the same state-and-command pattern, or return to [Choose your editor interface](/editor/ui/choose-an-interface) before expanding the toolbar.
