# Build your first custom control

> Move Bold into your application while SuperDoc renders the document and remaining toolbar.



Start from the [Editor quickstart](/editor/quickstart). Remove Bold from SuperDoc's toolbar, then render a Bold button in
your application. This is the smallest step from configuring the built-in UI to owning part of it.

## Try the ownership handoff [#try-the-ownership-handoff]

Select text in the document. Use **Bold** under **Your application**, then use **Italic** under **SuperDoc UI**. Both
controls should follow the same selection.

> **Live example: move one control into your application**
>
> Select text in the real DOCX. The application-owned Bold button and SuperDoc's remaining toolbar act on the same Editor selection. Bold is excluded from the built-in toolbar, but its command remains available through `superdoc.ui`.


Only the visible Bold control changed owner. SuperDoc still renders the DOCX canvas and the remaining toolbar.

## Build the same handoff [#build-the-same-handoff]

Choose Vanilla or React below. Your choice stays active across the examples.

If you chose React in the quickstart, add `superdoc` as a direct dependency. The custom UI hooks come from its
`superdoc/ui/react` entry point:

```bash
pnpm add superdoc
```

## 1. Move Bold into your application [#1-move-bold-into-your-application]

Vanilla needs a button next to its existing Editor container. React renders the button inside the component in the next
step and keeps the quickstart's `index.html`. Vanilla also needs a mount for the remaining built-in toolbar. If you use
Vanilla, replace `index.html` with:

```html
<div id="toolbar"></div>
<div aria-label="Document controls" role="toolbar">
  <button id="bold" type="button" disabled aria-pressed="false">Bold</button>
</div>
<output id="status" aria-live="polite" role="status">Select text to format it.</output>
<div id="editor"></div>

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

```

## 2. Connect Bold to the Editor [#2-connect-bold-to-the-editor]

Replace the quickstart Editor code:

**Vanilla — `src/main.ts`**

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

const boldButton = document.querySelector<HTMLButtonElement>('#bold');
const status = document.querySelector<HTMLOutputElement>('#status');
if (!boldButton || !status) throw new Error('The custom controls are missing.');

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

const editorUi = {
  toolbar: {
    container: '#toolbar',
    excludeItems: ['bold'],
  },
} satisfies UIConfig;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/sample.docx',
  ui: editorUi,
  onReady: ({ superdoc: readySuperDoc }) => {
    stopObserving?.();
    removeHandlers?.();
    const bold = readySuperDoc.ui.commands.get('bold');

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

    const onBoldClick = async () => {
      if (pending) return;
      const message = bold.getState().active ? 'Bold removed.' : 'Bold applied.';
      pending = true;
      render(bold.getState());
      try {
        const result = await bold.executeAsync();
        const applied = result === true || (typeof result === 'object' && result.success);
        status.textContent = applied ? message : 'Bold was not changed.';
      } finally {
        pending = false;
        render(bold.getState());
      }
    };

    const preserveSelection = (event: MouseEvent) => event.preventDefault();
    render(bold.getState());
    stopObserving = bold.observe(render);
    boldButton.addEventListener('mousedown', preserveSelection);
    boldButton.addEventListener('click', onBoldClick);
    removeHandlers = () => {
      boldButton.removeEventListener('mousedown', preserveSelection);
      boldButton.removeEventListener('click', onBoldClick);
    };
  },
  onContentError: ({ error }) => {
    status.textContent = 'The document could not be opened.';
    console.error(error);
  },
  onException: ({ error }) => {
    status.textContent = 'The document could not be opened.';
    console.error(error);
  },
});

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

```

**React — `src/App.tsx`**

```tsx
import { useState } from 'react';
import { SuperDocEditor } from '@superdoc/react';
import type { UIConfig } from 'superdoc';
import { SuperDocUIProvider, useSetSuperDoc, useSuperDocCommand } from 'superdoc/ui/react';
import '@superdoc/react/style.css';

const editorUi = {
  toolbar: { excludeItems: ['bold'] },
} satisfies UIConfig;

export default function App() {
  return (
    <SuperDocUIProvider>
      <BoldControl />
      <Editor />
    </SuperDocUIProvider>
  );
}

function BoldControl() {
  const bold = useSuperDocCommand('bold');
  const [pending, setPending] = useState(false);
  const [status, setStatus] = useState({ id: 0, message: 'Select text to format it.' });

  async function toggleBold() {
    if (pending) return;
    const message = bold.active ? 'Bold removed.' : 'Bold applied.';
    setPending(true);
    try {
      const result = await bold.executeAsync();
      const applied = result === true || (typeof result === 'object' && result.success);
      setStatus((current) => ({ id: current.id + 1, message: applied ? message : 'Bold was not changed.' }));
    } finally {
      setPending(false);
    }
  }

  return (
    <>
      <div aria-label='Document controls' role='toolbar'>
        <button
          aria-pressed={bold.active}
          disabled={!bold.enabled || pending}
          onClick={() => void toggleBold()}
          onMouseDown={(event) => event.preventDefault()}
          title={bold.reason ?? 'Toggle bold'}
          type='button'
        >
          Bold
        </button>
      </div>
      <output aria-live='polite' role='status'>
        <span key={status.id}>{status.message}</span>
      </output>
    </>
  );
}

function Editor() {
  const setSuperDoc = useSetSuperDoc();

  return (
    <SuperDocEditor
      document='/sample.docx'
      onContentError={({ error }) => console.error('SuperDoc could not open the document.', error)}
      onException={({ error }) => console.error('SuperDoc could not open the document.', error)}
      onReady={({ superdoc }) => setSuperDoc(superdoc)}
      ui={editorUi}
    />
  );
}

```


Both versions make the same ownership change:

* `toolbar.excludeItems` removes the built-in Bold button. The Bold command remains available.
* The application-owned button reads that command's state and runs it through `superdoc.ui`.
* The Editor owns the controller. Your application releases only the subscription and event handlers it created.

The `mousedown` handler keeps the document selection active when the custom button receives a mouse click. Keyboard
activation continues to use the button's normal focus and click behavior. [Selection and position](/editor/custom-ui/selection-and-viewport)
explains that pattern for menus, popovers, and other application UI.

## 3. Run the control [#3-run-the-control]

Run the project:

```bash
pnpm dev
```

Select text in the DOCX. **Bold** becomes enabled and reflects whether the selection is bold. Choose it and confirm that
the status reports whether bold was applied or removed. The remaining built-in toolbar controls should still render.

The [runnable custom UI example](https://go.superdoc.dev/examples/custom-ui) also verifies the exported DOCX.

Continue with [Commands and state](/editor/custom-ui/commands-and-state) to apply the same state and execution pattern to
other controls.
