Custom UI

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.

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. 1 Observeenabled · active
  2. 2 Renderdisabled · aria-pressed
  3. 3 ExecuteexecuteAsync()
  4. 4 Read outcomeboolean 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 toUse
Decide which built-in surfaces renderConfig.ui
Observe editor state and run UI actionssuperdoc.ui
Read or mutate an explicit document targeteditor.doc
Define what users are allowed to dointeraction

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 pending rather than throwing, so you can subscribe in the same tick as the constructor.
  • SuperDoc destroys it. superdoc.destroy() tears the controller down. superdoc.ui is typed BorrowedSuperDocUI, which omits destroy(), 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 stateUI behavior
enabled: falseDisable the control
enabled: trueAllow the command to run
active: trueShow a toggle as pressed
reasonExplain 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.

Interactive modelSelection drives command state

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.

Your toolbar
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 createdRelease it with
A subscriptionThe function observe() returned
The SuperDoc instancesuperdoc.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.

DefaultAdvanced
Get the controllerRead superdoc.uiCall createSuperDocUI()
Who owns itSuperDocYour application
Teardownsuperdoc.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.

On this page