Build application document controls
Add application-owned zoom and DOCX download while SuperDoc keeps the remaining built-in controls.
Zoom and download apply to the document or its view, not the current selection. Put them in your application header, observe the Editor state that keeps them current, and leave the rest of the built-in toolbar in place.
Try document-wide controls
- Use Zoom out, Zoom in, and Fit width. The document and percentage update together.
- Choose Download DOCX. The status reports when the export is ready.
The application owns these document-wide controls. SuperDoc still renders the Editor and the remaining toolbar without its Zoom control.
Build the same controls
Start from Custom UI setup with /sample.docx in your application's public
directory.
Add the controls
Vanilla adds mounts for the remaining built-in toolbar and the application controls. React creates the built-in toolbar
mount and renders the controls in the component below. If you use Vanilla, replace index.html with:
<div id="toolbar"></div>
<div class="document-controls" aria-label="Document controls" role="toolbar">
<button id="zoom-out" type="button" disabled>Zoom out</button>
<button id="fit-width" type="button" disabled>Fit width</button>
<button id="zoom-in" type="button" disabled>Zoom in</button>
<button id="export" type="button" disabled>Download DOCX</button>
<output id="document-status" aria-live="polite" role="status">Loading the document…</output>
</div>
<p id="document-error" role="alert"></p>
<div id="editor" style="height: 70vh"></div>
<script type="module" src="/src/main.ts"></script>
Connect them to the Editor
Replace the setup guide's Editor code:
import { SuperDoc } from 'superdoc';
import type { UIConfig } from 'superdoc';
import type { DocumentSlice } from 'superdoc/ui';
import 'superdoc/style.css';
const zoomOut = document.querySelector<HTMLButtonElement>('#zoom-out');
const fitWidth = document.querySelector<HTMLButtonElement>('#fit-width');
const zoomIn = document.querySelector<HTMLButtonElement>('#zoom-in');
const exportButton = document.querySelector<HTMLButtonElement>('#export');
const status = document.querySelector<HTMLOutputElement>('#document-status');
const errorMessage = document.querySelector<HTMLParagraphElement>('#document-error');
if (!zoomOut || !fitWidth || !zoomIn || !exportButton || !status || !errorMessage) {
throw new Error('The document controls are incomplete.');
}
let stopObservers: Array<() => void> = [];
let removeHandlers: (() => void) | null = null;
let actionMessage = '';
let exportInFlight = false;
function modeLabel(mode: DocumentSlice['mode']) {
if (mode === 'editing') return 'Editing';
if (mode === 'suggesting') return 'Suggesting';
if (mode === 'viewing') return 'Viewing';
return 'Ready';
}
const editorUi = {
toolbar: {
container: '#toolbar',
excludeItems: ['zoom'],
},
} satisfies UIConfig;
const superdoc = new SuperDoc({
selector: '#editor',
document: '/sample.docx',
ui: editorUi,
onReady: ({ superdoc: readySuperDoc }) => {
for (const stop of stopObservers) stop();
removeHandlers?.();
actionMessage = '';
exportInFlight = false;
errorMessage.textContent = '';
const ui = readySuperDoc.ui;
const render = () => {
const zoom = ui.zoom.getSnapshot();
const currentDocument = ui.document.getSnapshot();
zoomOut.disabled = zoom.value <= zoom.min;
zoomIn.disabled = zoom.value >= zoom.max;
fitWidth.disabled = false;
fitWidth.setAttribute('aria-pressed', String(zoom.mode === 'fit-width'));
exportButton.disabled = !currentDocument.ready || exportInFlight;
status.value = currentDocument.ready
? `${modeLabel(currentDocument.mode)} · ${zoom.value}%${actionMessage ? ` · ${actionMessage}` : ''}`
: 'Loading the document…';
};
const changeZoom = (delta: number) => {
const zoom = ui.zoom.getSnapshot();
ui.zoom.set(Math.min(zoom.max, Math.max(zoom.min, zoom.value + delta)));
};
const zoomOutHandler = () => changeZoom(-10);
const zoomInHandler = () => changeZoom(10);
const fitWidthHandler = () => ui.zoom.setMode('fit-width');
const exportHandler = async () => {
if (exportInFlight) return;
exportInFlight = true;
actionMessage = 'Preparing the DOCX…';
render();
try {
const pendingExport = ui.document.export({ exportType: ['docx'], triggerDownload: true });
if (!pendingExport) {
actionMessage = 'Export is unavailable in this host.';
return;
}
await pendingExport;
actionMessage = 'DOCX downloaded.';
} catch (error) {
actionMessage = error instanceof Error ? error.message : 'The DOCX could not be exported.';
} finally {
exportInFlight = false;
render();
}
};
stopObservers = [ui.zoom.observe(render), ui.document.observe(render)];
zoomOut.addEventListener('click', zoomOutHandler);
zoomIn.addEventListener('click', zoomInHandler);
fitWidth.addEventListener('click', fitWidthHandler);
exportButton.addEventListener('click', exportHandler);
removeHandlers = () => {
zoomOut.removeEventListener('click', zoomOutHandler);
zoomIn.removeEventListener('click', zoomInHandler);
fitWidth.removeEventListener('click', fitWidthHandler);
exportButton.removeEventListener('click', exportHandler);
};
},
onContentError: ({ error }) => {
errorMessage.textContent = 'The document could not be read or updated.';
console.error(error);
},
onException: ({ error }) => {
errorMessage.textContent = 'The editor reported a runtime error.';
console.error(error);
},
});
window.addEventListener('beforeunload', () => {
for (const stop of stopObservers) stop();
removeHandlers?.();
superdoc.destroy();
});
toolbar.excludeItems removes only Zoom. SuperDoc continues to render the other built-in toolbar controls. Vanilla also
sets toolbar.container because the application owns that mount. React creates one when container is omitted. Use
toolbar: false only when your application will render the complete toolbar.
ui.zoom reports the current percentage and supported range. set() switches to manual zoom. setMode('fit-width')
keeps the document fitted when the Editor container changes width. Zoom changes the view, not the DOCX.
The document snapshot's ready field tells you when document actions can run, and its mode field reports whether
the Editor is in editing, suggesting, or viewing mode. Read them from ui.document.getSnapshot() or receive them from
ui.document.observe(), as the examples do, so application controls follow both values without reading the rendered
document DOM.
ui.document.export() accepts the same typed DOCX options as superdoc.export(). It returns undefined when the bound
host cannot export. The example passes triggerDownload: true so a successful browser export downloads the returned
DOCX.
Verify the controls
Run the project. The built-in toolbar should render without Zoom. Change the zoom and download the DOCX. The application status should follow the current percentage and document mode.
Use Load and save documents to send exported bytes to your storage layer. Use Document modes to control editing, suggesting, and viewing.
This completes the core Custom UI path. Return to the Custom UI overview and choose the workflow your product needs.