# 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 [#try-document-wide-controls]

1. Use **Zoom out**, **Zoom in**, and **Fit width**. The document and percentage update together.
2. Choose **Download DOCX**. The status reports when the export is ready.

> **Live example: build document-wide controls without replacing the toolbar**
>
> The application renders Zoom, the current document mode, and Download DOCX. SuperDoc renders the remaining built-in toolbar without its Zoom control. Changing zoom updates the percentage, and Download DOCX exports the current document.


The application owns these document-wide controls. SuperDoc still renders the Editor and the remaining toolbar without
its Zoom control.

## Build the same controls [#build-the-same-controls]

Start from [Custom UI setup](/editor/custom-ui/controller-setup) with `/sample.docx` in your application's `public`
directory.

### Add the controls [#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:

```html
<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 [#connect-them-to-the-editor]

Replace the setup guide's Editor code:

**Vanilla — `src/main.ts`**

```ts
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();
});

```

**React — `src/App.tsx`**

```tsx
import { useRef, useState } from 'react';
import { SuperDocEditor } from '@superdoc/react';
import type { UIConfig } from 'superdoc';
import type { DocumentSlice } from 'superdoc/ui';
import {
  SuperDocUIProvider,
  useSetSuperDoc,
  useSuperDocDocument,
  useSuperDocUI,
  useSuperDocZoom,
} from 'superdoc/ui/react';
import '@superdoc/react/style.css';

const editorUi = {
  toolbar: { excludeItems: ['zoom'] },
} satisfies UIConfig;

function modeLabel(mode: DocumentSlice['mode']) {
  if (mode === 'editing') return 'Editing';
  if (mode === 'suggesting') return 'Suggesting';
  if (mode === 'viewing') return 'Viewing';
  return 'Ready';
}

export default function App() {
  const [errorMessage, setErrorMessage] = useState('');

  return (
    <SuperDocUIProvider>
      <DocumentControls errorMessage={errorMessage} />
      <Editor onError={setErrorMessage} />
    </SuperDocUIProvider>
  );
}

function DocumentControls({ errorMessage }: { errorMessage: string }) {
  const ui = useSuperDocUI();
  const zoom = useSuperDocZoom();
  const documentState = useSuperDocDocument();
  const [actionMessage, setActionMessage] = useState('');
  const [isExporting, setIsExporting] = useState(false);
  const exportInFlight = useRef(false);

  function changeZoom(delta: number) {
    ui?.zoom.set(Math.min(zoom.max, Math.max(zoom.min, zoom.value + delta)));
  }

  async function downloadDocx() {
    if (exportInFlight.current) return;

    exportInFlight.current = true;
    setIsExporting(true);
    setActionMessage('Preparing the DOCX…');
    try {
      const pendingExport = ui?.document.export({ exportType: ['docx'], triggerDownload: true });
      if (!pendingExport) {
        setActionMessage('Export is unavailable in this host.');
        return;
      }
      await pendingExport;
      setActionMessage('DOCX downloaded.');
    } catch (error) {
      setActionMessage(error instanceof Error ? error.message : 'The DOCX could not be exported.');
    } finally {
      exportInFlight.current = false;
      setIsExporting(false);
    }
  }

  const status = documentState.ready
    ? `${modeLabel(documentState.mode)} · ${zoom.value}%${actionMessage ? ` · ${actionMessage}` : ''}`
    : 'Loading the document…';

  return (
    <>
      <div aria-label='Document controls' role='toolbar'>
        <button disabled={!ui || zoom.value <= zoom.min} onClick={() => changeZoom(-10)} type='button'>
          Zoom out
        </button>
        <button
          aria-pressed={zoom.mode === 'fit-width'}
          disabled={!ui}
          onClick={() => ui?.zoom.setMode('fit-width')}
          type='button'
        >
          Fit width
        </button>
        <button disabled={!ui || zoom.value >= zoom.max} onClick={() => changeZoom(10)} type='button'>
          Zoom in
        </button>
        <button disabled={!ui || !documentState.ready || isExporting} onClick={() => void downloadDocx()} type='button'>
          Download DOCX
        </button>
        <output aria-live='polite' role='status'>
          {status}
        </output>
      </div>
      {errorMessage ? <p role='alert'>{errorMessage}</p> : null}
    </>
  );
}

function Editor({ onError }: { onError: (message: string) => void }) {
  const setSuperDoc = useSetSuperDoc();

  return (
    <SuperDocEditor
      document='/sample.docx'
      onContentError={({ error }) => {
        onError('The document could not be read or updated.');
        console.error(error);
      }}
      onException={({ error }) => {
        onError('The editor reported a runtime error.');
        console.error(error);
      }}
      onReady={({ superdoc }) => {
        onError('');
        setSuperDoc(superdoc);
      }}
      ui={editorUi}
    />
  );
}

```


`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 [#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](/editor/load-and-save-documents) to send exported bytes to your storage layer. Use
[Document modes](/editor/document-modes) to control editing, suggesting, and viewing.

This completes the core Custom UI path. Return to the [Custom UI overview](/editor/custom-ui/overview) and choose the
workflow your product needs.
