Custom UI overview
Build application-owned controls over the public SuperDoc v2 UI controller.
A custom UI keeps SuperDoc's DOCX canvas and editing behavior while your application owns the toolbar, panels, and workflow around it.
SuperDoc owns the DOCX canvas, layout, selection, and editing behavior. Your application builds the controls and workflow around it using reactive controller state, React hooks, commands, workflow methods, and the Document API.
A SuperDoc editor renders a DOCX document in the centre. A toolbar above it and a comments panel beside it are rendered by the application. Reactive state — selection, command state, and comments — flows out of the editor to those controls, and commands and document operations flow back in.
SuperDoc renders the document, layout, selection, and editing
Your comments panel
renders from the comments snapshot
state out — selection, command state, comments
actions in — commands and document operations
Use this approach when a general-purpose document toolbar does not match the product task. A contract approval screen, structured intake flow, or focused review experience may need a smaller set of controls and application-specific state.
Config.ui chooses which built-in surfaces SuperDoc renders. It does not remove the underlying capabilities: turning off the comments panel leaves comment data and every comment operation intact.
Try one custom control
Select text in the document below, then use the Bold button. It is application code: it reads enabled and active from the controller and executes through it, while SuperDoc keeps rendering the document.
- 1 Observe
enabled · active - 2 Render
disabled · aria-pressed - 3 Execute
executeAsync() - 4 Read outcome
boolean or receipt
That button demonstrates four habits for command controls, highlighted in the strip beneath it as you use them. aria-pressed is specific to toggles like Bold; a control that is not a toggle skips step 2's second half, and a panel that reads document content rather than running a command follows a different path entirely.
The custom UI example is a complete runnable project containing only this control and DOCX export.
That control is this much code, using nothing but the public surface:
const bold = superdoc.ui.commands.get('bold');
const stop = bold.observe((state) => {
button.disabled = !state.enabled;
button.setAttribute('aria-pressed', String(state.active));
});
button.addEventListener('click', async () => {
const result = await bold.executeAsync();
// Inspect `result` when later work depends on the command having applied.
});Call stop() when the control unmounts. The rest of this page describes the contract that makes those three lines work.
Four public surfaces
| You need to | Use |
|---|---|
| Decide which built-in surfaces render | Config.ui |
| Observe editor state and run UI actions | superdoc.ui |
| Read or mutate an explicit document target | editor.doc |
| Define what users are allowed to do | interaction |
superdoc/ui/react provides the same controller through a provider and hooks. Use the framework-neutral controller when your application does not use React or when UI state belongs outside a component tree.
Config.ui and interaction look similar and are not. ui: { comments: false } hides the built-in comments interface while your own panel keeps creating comments; interaction: { comments: { readOnly: true } } is what refuses the write.
Use the controller SuperDoc already owns
Every SuperDoc instance exposes its own UI controller as superdoc.ui. It is the shortest path to custom controls and the only path available when SuperDoc is loaded through a classic <script> tag, where package subpath imports are unavailable.
const superdoc = new SuperDoc({ selector: '#editor', document: file });
const stop = superdoc.ui.comments.observe((comments) => renderSidebar(comments.items));
const bold = superdoc.ui.commands.get('bold');Four properties of superdoc.ui matter when you wire an application to it:
- One canonical controller. Every SuperDoc instance owns one controller at
superdoc.ui. SuperDoc's own toolbar, popovers, and keyboard routing read that same object, so your controls and the built-in ones never disagree about command state. - Stable identity. Reading it twice returns the same controller. Replacing the document or switching the active editor in a multi-document instance does not replace it.
- Safe before the document is ready. Slices report
pendingrather than throwing, so you can subscribe in the same tick as the constructor. - SuperDoc destroys it.
superdoc.destroy()tears the controller down.superdoc.uiis typedBorrowedSuperDocUI, which omitsdestroy(), so you cannot accidentally tear down state that every other consumer is observing. Release your own subscriptions and leave the controller alone.
superdoc.ui is an observation and command surface, not a permission boundary. Anything it exposes is already reachable by the page hosting SuperDoc.
The controller provides:
- Immutable snapshots for selection, toolbar, comments, tracked changes, content controls, fonts, zoom, document state, styles, and search.
- Subscriptions that update controls when editor state changes.
- Command handles with
enabled,active,value, and stable disabled reasons. - Async command execution with receipts or explicit failure results.
- Selection capture, viewport navigation, and document-aware workflow helpers.
- One
destroy()boundary for controller subscriptions and listeners.
Both forms expose the same surface.
Let controller state drive your controls
When the selection or document state changes, SuperDoc publishes new control state. For command availability, read the command handle's enabled state rather than deriving it from the selection yourself.
| Controller state | UI behavior |
|---|---|
enabled: false | Disable the control |
enabled: true | Allow the command to run |
active: true | Show a toggle as pressed |
reason | Explain why it is unavailable |
For example, a Bold button should use the bold command handle's state. The controller decides whether the current selection can be formatted and whether bold is already active, which is what the live example above reads.
The model below is a faster way to see all three states at once, including the disabled case that needs specific content to reproduce. It simulates the selection rather than running an Editor.
Choose sample content to see the state a custom Bold control receives. The selection is simulated. A real controller derives these values from the active Editor selection.
- enabled
- true
- active
- false
- reason
- undefined
Check the result when the next step depends on success
A command can be refused without throwing. If the next step is to save, navigate, close a dialog, or report success, inspect the result first — the absence of an exception is not the same as the document having changed.
executeAsync() resolves once the routed work settles. It returns either a boolean or a Document API receipt, so read both shapes when you branch on it.
Clean up what you own
One rule: clean up what you created, and nothing else.
| You created | Release it with |
|---|---|
| A subscription | The function observe() returned |
The SuperDoc instance | superdoc.destroy() |
A controller from createSuperDocUI() | Its own destroy(), before the instance |
superdoc.ui is not on that list. SuperDoc created it, so superdoc.destroy() takes it down.
Advanced: construct a separate controller
Most applications do not need this.
| Default | Advanced | |
|---|---|---|
| Get the controller | Read superdoc.ui | Call createSuperDocUI() |
| Who owns it | SuperDoc | Your application |
| Teardown | superdoc.destroy() | Both, controller first |
createSuperDocUI from superdoc/ui is an ownership escape hatch, not the normal path. It binds to a SuperDoc instance and exposes the same surface, but you own its lifecycle: you create it and you call its destroy().
Mounting and unmounting an ordinary panel is not a reason to build one. A panel can release its own subscriptions and keep sharing superdoc.ui, which is less to own and less to get wrong. Reach for the factory when a consumer genuinely needs a controller whose lifetime is independent of the Editor's — a detached preview driven from its own state, for example.
Destroying a controller you built this way tears down only that controller. superdoc.ui, the built-in toolbar, and every other consumer keep working, so an independent lifecycle is a supported choice rather than a risky one.
Prefer superdoc.ui everywhere else.
Continue with Custom UI controller setup to build this control yourself against a real DOCX, then expand it into a formatting toolbar.