Custom UI

Control zoom and document state

Build fit-to-width, manual zoom, dirty-state, and export controls around the active document.

Use ui.zoom for viewport scale and ui.document for readiness, mode, dirty state, export, and file replacement. These handles keep application chrome synchronized with the active Editor.

Complete Custom UI controller setup first. This example expects /contract.docx to be available from your application.

Add document controls

Create zoom, export, and status controls next to the Editor:

<div class="document-controls" aria-label="Document controls">
  <button id="zoom-out" type="button">Zoom out</button>
  <button id="fit-width" type="button">Fit width</button>
  <button id="zoom-in" type="button">Zoom in</button>
  <button id="export" type="button">Download DOCX</button>
  <output id="document-status" aria-live="polite">Loading the document…</output>
</div>
<div id="editor" style="height: 70vh"></div>

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

Observe both snapshots

Bind the controls after the Editor is ready:

import { SuperDoc } from 'superdoc';
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');

if (!zoomOut || !fitWidth || !zoomIn || !exportButton || !status) {
  throw new Error('The document controls are incomplete.');
}

let stopObservers: Array<() => void> = [];
let removeHandlers: (() => void) | null = null;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  onReady: ({ superdoc: readySuperDoc }) => {
    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.setAttribute('aria-pressed', String(zoom.mode === 'fit-width'));
      exportButton.disabled = !currentDocument.ready;
      status.value = currentDocument.ready
        ? `${currentDocument.dirty ? 'Unsaved changes' : 'Saved'} · ${zoom.value}% · ${currentDocument.mode ?? 'loading'}`
        : 'Loading the document…';
    };

    const changeZoom = (delta: number) => {
      const zoom = ui.zoom.getSnapshot();
      ui.zoom.setMode('manual');
      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 () => {
      status.value = 'Preparing the DOCX…';
      const pendingExport = ui.document.export({ exportType: ['docx'] });
      if (!pendingExport) {
        status.value = 'Export is unavailable in this host.';
        return;
      }
      try {
        await pendingExport;
        status.value = 'DOCX downloaded.';
      } catch (error) {
        status.value = error instanceof Error ? error.message : 'The DOCX could not be exported.';
      }
    };

    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);
    };
  },
});

window.addEventListener('beforeunload', () => {
  for (const stop of stopObservers) stop();
  removeHandlers?.();
  superdoc.destroy();
});

ui.zoom.getSnapshot() returns the current mode, percentage, and supported range. Setting a percentage switches the example to manual mode. Fit-to-width remains responsive as the Editor container changes size, so it is the better default for flexible layouts.

ui.document.getSnapshot() reports whether the Editor is ready, its mode, and whether it has unsaved changes. Treat dirty as a prompt to save or export, not as proof that a remote persistence request succeeded.

Handle unavailable host operations

ui.document.export() and replaceFile() return undefined when the host does not expose the operation. Check before awaiting. Export triggers a browser download by default; pass { triggerDownload: false } when your application needs the result for upload instead.

ui.zoom.set(), ui.zoom.setMode(), and ui.document.setMode() are control methods, not mutation receipts. Observe the resulting snapshot. When later work depends on a document-mode change, use the document-mode command and await executeAsync() instead.

For file loading policies and export options, read Load and save documents. For the meaning of editing, suggesting, and viewing, read Document modes.

On this page