Build your first custom control
Move Bold into your application while SuperDoc renders the document and remaining toolbar.
Start from the 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
Select text in the document. Use Bold under Your application, then use Italic under SuperDoc UI. Both controls should follow the same selection.
Only the visible Bold control changed owner. SuperDoc still renders the DOCX canvas and the remaining toolbar.
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:
pnpm add superdoc1. 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:
<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
Replace the quickstart Editor code:
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();
});
Both versions make the same ownership change:
toolbar.excludeItemsremoves 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
explains that pattern for menus, popovers, and other application UI.
3. Run the control
Run the project:
pnpm devSelect 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 also verifies the exported DOCX.
Continue with Commands and state to apply the same state and execution pattern to other controls.