# 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](/editor/custom-ui/controller-setup) first. This example expects `/contract.docx` to be available from your application.

## Add document controls [#add-document-controls]

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

```html
<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 [#observe-both-snapshots]

Bind the controls after the Editor is ready:

```ts
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 [#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](/editor/load-and-save-documents). For the meaning of editing, suggesting, and viewing, read [Document modes](/editor/document-modes).
