Skip to main content
Surfaces let you show dialogs and floating overlays above the document. Use them for things like:
  • password prompts
  • find and replace
  • lightweight tools or inspectors
  • custom workflow dialogs
SuperDoc supports two surface modes:
  • dialog — modal, centered, blocks interaction behind it
  • floating — non-modal, pinned to the visible document area

When to configure surfaces up front

Most apps do not need any extra setup to use surfaces. If you are opening a surface directly with superdoc.openSurface(...), you can do that with a normal SuperDoc instance and no special config. Add modules.surfaces only when you want:
  • global defaults for dialogs or floating overlays
  • a central resolver for intent-based requests using kind
  • built-in surface behaviors such as opt-in find/replace or password prompt customization

Open a surface

superdoc.openSurface(request)

Open a dialog or floating surface above the document. Returns: SurfaceHandle
request
Object
required
Surface request
Provide either:
  • component or render for a direct surface request
  • kind for a resolver-based request
component and render are mutually exclusive.

Dialog example

const handle = superdoc.openSurface({
  mode: 'dialog',
  title: 'Confirm action',
  dialog: { maxWidth: 420 },
  render: ({ container, close, resolve }) => {
    container.innerHTML = `
      <div style="display:grid;gap:12px;">
        <p style="margin:0;">Continue?</p>
        <div style="display:flex;gap:8px;justify-content:flex-end;">
          <button type="button" data-cancel>Cancel</button>
          <button type="button" data-ok>Confirm</button>
        </div>
      </div>
    `;

    container.querySelector('[data-cancel]')?.addEventListener('click', () => close('cancel'));
    container.querySelector('[data-ok]')?.addEventListener('click', () => resolve({ confirmed: true }));
  },
});

Floating example

const handle = superdoc.openSurface({
  mode: 'floating',
  title: 'Find',
  floating: { placement: 'top-right', width: 360 },
  render: ({ container, close }) => {
    container.innerHTML = `
      <div style="display:grid;gap:12px;">
        <input placeholder="Find..." style="width:100%" />
        <button type="button">Close</button>
      </div>
    `;

    container.querySelector('button')?.addEventListener('click', () => close('manual-close'));
  },
});

Close a surface

superdoc.closeSurface(id?)

Close a surface programmatically.
  • pass an id to close a specific surface
  • omit id to close the topmost active surface
  • if both are open, dialog closes before floating
const handle = superdoc.openSurface({
  mode: 'floating',
  title: 'Find',
  render: ({ container }) => {
    container.innerHTML = '<input placeholder="Find..." style="width:100%" />';
  },
});

superdoc.closeSurface(handle.id);
// or:
superdoc.closeSurface();

Handle lifecycle

openSurface() returns a handle with:
  • id
  • mode
  • close(reason?)
  • result
handle.result always resolves. It does not reject for normal lifecycle events. Possible results:
  • { status: 'submitted', data }
  • { status: 'closed', reason }
  • { status: 'replaced', replacedBy }
  • { status: 'destroyed' }
const handle = superdoc.openSurface({
  mode: 'dialog',
  title: 'Example',
  render: ({ container, resolve }) => {
    container.innerHTML = '<button type="button">Done</button>';
    container.querySelector('button')?.addEventListener('click', () => resolve({ ok: true }));
  },
});

const outcome = await handle.result;
console.log(outcome.status, outcome.data);
There is one active dialog slot and one active floating slot. Opening a new surface in the same mode replaces the previous one.

Vue component content

For Vue consumers, component is the simplest path.
import MyFindSurface from './MyFindSurface.vue';

superdoc.openSurface({
  mode: 'floating',
  title: 'Find',
  floating: { placement: 'top-right', width: 360 },
  component: MyFindSurface,
  props: {
    initialQuery: 'contract',
  },
});
Your component receives:
  • surfaceId
  • mode
  • request
  • resolve(data?)
  • close(reason?)

React and other frameworks

For React, use render() and mount into the provided container.
import { createRoot } from 'react-dom/client';
import { FindOverlay } from './FindOverlay';

superdoc.openSurface({
  mode: 'floating',
  title: 'Find',
  floating: { placement: 'top-right', width: 360 },
  render: ({ container, request, close, resolve }) => {
    const root = createRoot(container);

    root.render(
      <FindOverlay
        request={request}
        onClose={() => close('user-close')}
        onSubmit={(data) => resolve(data)}
      />
    );

    return {
      destroy() {
        root.unmount();
      },
    };
  },
});
This is the same pattern as external link popovers: SuperDoc owns the shell, and your app mounts content into the provided container.

Optional instantiation config

Add modules.surfaces only if you want shared defaults, a resolver, or built-in surface behaviors such as find/replace.
new SuperDoc({
  selector: '#editor',
  document: file,
  modules: {
    surfaces: {
      dialog: {
        maxWidth: 520,
      },
      floating: {
        placement: 'top-right',
        width: 360,
        maxHeight: 320,
      },
    },
  },
});
See the full list of available defaults in the configuration reference.

Resolver-based surfaces

Use a resolver when you want to open a surface by intent instead of passing a renderer each time.
new SuperDoc({
  selector: '#editor',
  document: file,
  modules: {
    surfaces: {
      resolver: (request) => {
        if (request.kind === 'find') {
          return {
            type: 'external',
            render: ({ container, close }) => {
              container.innerHTML = '<input placeholder="Find..." style="width:100%" />';
              return {};
            },
          };
        }
      },
    },
  },
});

superdoc.openSurface({
  kind: 'find',
  mode: 'floating',
  title: 'Find',
});
There is no built-in surface registry yet. If you use kind, you must provide modules.surfaces.resolver.

Built-in Find and Replace

SuperDoc includes a built-in find/replace popover for editor-backed documents. It is disabled by default so existing apps can keep their own Cmd+F / Ctrl+F handling. When enabled, SuperDoc intercepts those shortcuts while focus is inside SuperDoc and opens the built-in UI. From shortcut handling, SuperDoc only steals Cmd+F / Ctrl+F when it can actually open a surface. If findReplace.resolver returns { type: 'none' }, or the config is invalid or throws, the browser’s native find remains active. Enable it via modules.surfaces.findReplace:
new SuperDoc({
  selector: '#editor',
  document: file,
  modules: {
    surfaces: {
      findReplace: true,
    },
  },
});

Text customization

Override any of the built-in labels and placeholders:
modules: {
  surfaces: {
    findReplace: {
      findPlaceholder: 'Search in document',
      replacePlaceholder: 'Replace with',
      noResultsLabel: 'Nothing found',
      previousMatchLabel: 'Previous result',
      nextMatchLabel: 'Next result',
      closeAriaLabel: 'Close search',
    },
  },
}

Find-only mode

Disable replace actions and keep only the find UI:
modules: {
  surfaces: {
    findReplace: {
      replaceEnabled: false,
    },
  },
}
modules.surfaces.findReplace
false | true | Object
default:"false"
Find/replace configuration. Disabled by default; set to true to enable the built-in UI, or pass an object to customize labels, disable replace actions, or replace the rendering.

Custom Vue component

Replace the built-in content with your own Vue component. Your component receives a findReplace prop with reactive state and actions:
import MyFindReplaceSurface from './MyFindReplaceSurface.vue';

modules: {
  surfaces: {
    findReplace: {
      component: MyFindReplaceSurface,
    },
  },
}
Inside your component:
<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
  findReplace: { type: Object, required: true },
});

const inputRef = ref(null);

onMounted(() => {
  props.findReplace.registerFocusFn(() => inputRef.value?.focus());
});
</script>

<template>
  <input
    ref="inputRef"
    :value="findReplace.findQuery.value"
    @input="findReplace.findQuery.value = $event.target.value"
  />
  <button @click="findReplace.goPrev()">Prev</button>
  <button @click="findReplace.goNext()">Next</button>
</template>

Custom render function (React, vanilla JS)

Use render for framework-agnostic mounting. The render function receives a FindReplaceRenderContext:
modules: {
  surfaces: {
    findReplace: {
      render: (ctx) => {
        const input = document.createElement('input');
        input.value = ctx.findReplace.findQuery;
        input.addEventListener('input', (event) => {
          ctx.findReplace.findQuery = event.target.value;
        });

        ctx.findReplace.registerFocusFn(() => input.focus());
        ctx.container.appendChild(input);

        return { destroy: () => input.remove() };
      },
    },
  },
}
ctx exposes container, findReplace, resolve, close, surfaceId, and mode. ctx.findReplace exposes plain JavaScript getters/setters instead of Vue refs so non-Vue renderers can work with it directly.

Conditional resolver

Use resolver when your app wants to decide at runtime whether to use built-in, custom, external, or suppressed rendering. The resolver receives a read-only context with texts and replaceEnabled:
modules: {
  surfaces: {
    findReplace: {
      replaceEnabled: false,
      resolver: (ctx) => {
        if (!ctx.replaceEnabled) {
          return { type: 'custom', component: ReadOnlyFindSurface };
        }
        return null; // fall through to built-in
      },
    },
  },
}
Resolution types:
TypeBehavior
null / undefinedFall through to component/render or built-in
{ type: 'default' }Same as null — fall through
{ type: 'none' }Suppress SuperDoc’s find/replace surface for this open attempt
{ type: 'custom', component, props? }Mount a Vue component in the floating shell
{ type: 'external', render }Mount framework-agnostic UI in the floating shell
The resolver can coexist with component/render. If the resolver returns null or { type: 'default' }, the direct component/render is used. If neither is configured, the built-in popover renders. Precedence: resolver → component/render → built-in.

The findReplace handle

Custom Vue components receive findReplace as a Vue handle with refs/computed refs. External render functions receive ctx.findReplace with the same API surface, but mutable fields are exposed as plain JavaScript getters/setters and derived fields are plain getters.
FieldVue component valueExternal render valueDescription
findQueryRef<string>string getter/setterCurrent search query
replaceTextRef<string>string getter/setterCurrent replacement text
caseSensitiveRef<boolean>boolean getter/setterMatch-case toggle
ignoreDiacriticsRef<boolean>boolean getter/setterIgnore-diacritics toggle
showReplaceRef<boolean>boolean getter/setterWhether the replace row is expanded
matchCountRef<number>number getterTotal match count
activeMatchIndexRef<number>number getterActive match index, -1 when none
matchLabelComputedRef<string>string getterFormatted label such as 3 of 12 or No results
hasMatchesComputedRef<boolean>boolean getterWhether there are any matches
replaceEnabledbooleanbooleanWhether replace actions are available
textsResolvedFindReplaceTextsResolvedFindReplaceTextsAll text strings resolved with defaults
goNext / goPrev() => void() => voidNavigate through matches
replaceCurrent / replaceAll() => void() => voidRun replacement actions
registerFocusFn(fn) => void(fn) => voidRegister the function SuperDoc calls when the user presses Cmd+F / Ctrl+F again while the surface is already open
close(reason?: unknown) => void(reason?: unknown) => voidClose the surface

{ type: 'none' } semantics

{ type: 'none' } means suppress SuperDoc’s find/replace surface for that open attempt. When find/replace is opened via Cmd+F / Ctrl+F, suppression falls back to the browser’s native find dialog instead of swallowing the shortcut.

Built-in password prompt

SuperDoc includes a built-in password dialog for encrypted DOCX files. Enabled by default. On wrong password, the dialog stays open and shows an error. On success, the document loads normally.
new SuperDoc({
  selector: '#editor',
  document: encryptedFile,
  modules: {
    surfaces: {
      passwordPrompt: true, // default — can omit
    },
  },
});
Set to false to disable:
modules: {
  surfaces: {
    passwordPrompt: false,
  },
}

Text customization

Override any of the built-in copy:
modules: {
  surfaces: {
    passwordPrompt: {
      title: 'Unlock document',
      invalidTitle: 'Wrong password — try again',
      description: 'This file requires a password.',
      submitLabel: 'Unlock',
      cancelLabel: 'Dismiss',
      busyLabel: 'Opening\u2026',
      invalidMessage: 'That password is wrong. Try again.',
      timeoutMessage: 'Took too long. Try again.',
      genericErrorMessage: 'Could not open this file.',
    },
  },
}
modules.surfaces.passwordPrompt
false | true | Object
default:"true"
Password prompt configuration. Enabled by default when omitted; set to false to disable.

Custom Vue component

Replace the built-in dialog content with your own Vue component. Your component receives a passwordPrompt prop with everything it needs:
import MyPasswordDialog from './MyPasswordDialog.vue';

modules: {
  surfaces: {
    passwordPrompt: {
      component: MyPasswordDialog,
    },
  },
}
Inside your component:
<script setup>
const props = defineProps({
  passwordPrompt: { type: Object, required: true },
  resolve: { type: Function, required: true },
  close: { type: Function, required: true },
});

async function handleSubmit(password) {
  const result = await props.passwordPrompt.attemptPassword(password);
  if (result.success) {
    props.resolve(); // no args required
  }
  // result.errorCode tells you what went wrong
}
</script>

Custom render function (React, vanilla JS)

Use render for framework-agnostic mounting. The render function receives a PasswordPromptRenderContext:
modules: {
  surfaces: {
    passwordPrompt: {
      render: (ctx) => {
        // ctx.container — empty DOM element to render into
        // ctx.passwordPrompt — the handle (documentId, errorCode, texts, attemptPassword)
        // ctx.resolve — call on success
        // ctx.close — call to dismiss

        const root = ReactDOM.createRoot(ctx.container);
        root.render(<MyPasswordPrompt passwordPrompt={ctx.passwordPrompt} resolve={ctx.resolve} close={ctx.close} />);
        return { destroy: () => root.unmount() };
      },
    },
  },
}

Conditional resolver

Use resolver when you need per-document decisions. The resolver receives a read-only PasswordPromptContext (documentId, errorCode, texts) and returns a resolution:
modules: {
  surfaces: {
    passwordPrompt: {
      resolver: (ctx) => {
        if (ctx.documentId === 'public-doc') {
          return { type: 'none' }; // suppress prompt for this doc
        }
        return null; // fall through to built-in
      },
    },
  },
}
Resolution types:
TypeBehavior
null / undefinedFall through to component/render or built-in
{ type: 'default' }Same as null — fall through
{ type: 'none' }Suppress the password prompt for this document
{ type: 'custom', component, props? }Mount a Vue component in the dialog shell
{ type: 'external', render }Mount framework-agnostic UI in the dialog shell
The resolver can coexist with component/render. If the resolver returns null or { type: 'default' }, the direct component/render is used. If neither is configured, the built-in prompt renders. Precedence: resolver → component/render → built-in.

The passwordPrompt handle

Every custom UI (component or render) receives a passwordPrompt handle with this shape:
FieldTypeDescription
documentIdstringThe document ID requiring a password
errorCodestring'DOCX_PASSWORD_REQUIRED' or 'DOCX_PASSWORD_INVALID'
textsResolvedPasswordPromptTextsAll 11 text strings resolved with defaults
attemptPassword(password: string) => Promise<{ success, errorCode? }>Submit a password attempt

How actions work

Submit / unlock button:
  1. Call await passwordPrompt.attemptPassword(password)
  2. If { success: true } — call resolve() to close the dialog
  3. If { success: false, errorCode } — keep the dialog open, show your error state
resolve() with no arguments is sufficient. The password flow does not consume the resolved data. Cancel / dismiss button:
  • Call close('user-cancelled')
Other actions (forgot password, help links): app-owned. Keep the dialog open or close with a custom reason.
Do not set doc.password or increment editorMountNonce directly. Use passwordPrompt.attemptPassword(password) — it handles the document mutation and remount internally.

{ type: 'none' } semantics

{ type: 'none' } means suppress SuperDoc’s password prompt. The resolver context does not expose attemptPassword, so none cannot be used to build a self-hosted modal that retries passwords through SuperDoc. Use this when your app handles passwords entirely outside SuperDoc — for example, pre-prompting before instantiation or server-side decryption. For global suppression, use passwordPrompt: false instead.

Relationship with the exception event

If passwordPrompt is disabled, encrypted files emit DOCX_PASSWORD_REQUIRED or DOCX_PASSWORD_INVALID on the exception event. Your app can handle password entry through that event instead. When passwordPrompt is enabled, recoverable encryption errors are intercepted by the password prompt flow. If the prompt renders successfully (built-in, custom, or external), the exception event is suppressed. If the prompt cannot render (resolver returned { type: 'none' }, invalid config, or resolver threw), the original exception event is re-emitted so your app can handle it.

Multi-document handling

When multiple encrypted documents are loaded, the password prompt queues one dialog at a time in FIFO order. After the user submits or cancels for one document, the next dialog appears.

Styling

Background, shadow, border radius, padding, and edge offset are all customizable via --sd-ui-surface-* and --sd-ui-floating-* CSS variables. Find/replace controls use --sd-ui-find-replace-*, and search highlight colors use --sd-ui-search-match-bg plus --sd-ui-search-match-active-bg. See the full token table in Custom themes.