> ## Documentation Index
> Fetch the complete documentation index at: https://superdoc-caio-pizzol-docs-ai-core-preset.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Surfaces

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`

<ParamField path="request" type="Object" required>
  Surface request

  <Expandable title="common fields" defaultOpen>
    <ParamField path="mode" type="'dialog' | 'floating'" required>
      Presentation mode
    </ParamField>

    <ParamField path="title" type="string">
      Optional title rendered in the surface chrome
    </ParamField>

    <ParamField path="ariaLabel" type="string">
      Accessible name for the surface when no visible `title` is provided. Used as `aria-label` on the surface element (dialog or floating). Ignored when `title` or `ariaLabelledBy` is set.
    </ParamField>

    <ParamField path="closeOnEscape" type="boolean" default="true">
      Whether `Escape` closes the surface
    </ParamField>
  </Expandable>

  <Expandable title="dialog fields">
    <ParamField path="closeOnBackdrop" type="boolean" default="true">
      Whether clicking the backdrop closes the dialog
    </ParamField>

    <ParamField path="dialog.maxWidth" type="string | number">
      Dialog max-width override
    </ParamField>
  </Expandable>

  <Expandable title="floating fields">
    <ParamField path="floating.placement" type="'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'" default="'top-right'">
      Placement preset. Ignored when exact insets are provided.
    </ParamField>

    <ParamField path="floating.top" type="string | number">
      Exact top inset
    </ParamField>

    <ParamField path="floating.right" type="string | number">
      Exact right inset
    </ParamField>

    <ParamField path="floating.bottom" type="string | number">
      Exact bottom inset
    </ParamField>

    <ParamField path="floating.left" type="string | number">
      Exact left inset
    </ParamField>

    <ParamField path="floating.width" type="string | number">
      Width override
    </ParamField>

    <ParamField path="floating.maxWidth" type="string | number">
      Max width override
    </ParamField>

    <ParamField path="floating.maxHeight" type="string | number">
      Max height override
    </ParamField>

    <ParamField path="floating.autoFocus" type="boolean" default="true">
      Move focus into the first focusable child on open
    </ParamField>

    <ParamField path="floating.closeOnOutsidePointerDown" type="boolean" default="false">
      Close when clicking outside the floating surface
    </ParamField>
  </Expandable>

  <Expandable title="rendering fields">
    <ParamField path="component" type="Vue Component">
      Vue component rendered inside the built-in surface shell. Receives `surfaceId`, `mode`, `request`, `resolve`, and `close` as props.
    </ParamField>

    <ParamField path="props" type="Record<string, unknown>">
      Extra props passed to a Vue component
    </ParamField>

    <ParamField path="render" type="(ctx) => { destroy?: () => void } | void">
      Framework-agnostic renderer. Use this for React or manual DOM mounting.
    </ParamField>

    <ParamField path="kind" type="string">
      Intent key for resolver-based surfaces
    </ParamField>
  </Expandable>
</ParamField>

<Note>
  Provide either:

  * `component` or `render` for a direct surface request
  * `kind` for a resolver-based request

  `component` and `render` are mutually exclusive.
</Note>

### Dialog example

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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' }`

```javascript theme={null}
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);
```

<Note>
  There is one active `dialog` slot and one active `floating` slot. Opening a new surface in the same mode replaces the previous one.
</Note>

## Vue component content

For Vue consumers, `component` is the simplest path.

```javascript theme={null}
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`.

```jsx theme={null}
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](/editor/built-in-ui/links#external-renderer): 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.

```javascript theme={null}
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](/editor/superdoc/configuration#surface-defaults).

## Resolver-based surfaces

Use a resolver when you want to open a surface by intent instead of passing a renderer each time.

```javascript theme={null}
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',
});
```

<Warning>
  There is no built-in surface registry yet. If you use `kind`, you must provide `modules.surfaces.resolver`.
</Warning>

## 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`:

```javascript theme={null}
new SuperDoc({
  selector: '#editor',
  document: file,
  modules: {
    surfaces: {
      findReplace: true,
    },
  },
});
```

### Text customization

Override any of the built-in labels and placeholders:

```javascript theme={null}
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:

```javascript theme={null}
modules: {
  surfaces: {
    findReplace: {
      replaceEnabled: false,
    },
  },
}
```

<ParamField path="modules.surfaces.findReplace" type="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.

  <Expandable title="text fields">
    <ParamField path="findPlaceholder" type="string" default="'Find'">
      Placeholder text for the find input
    </ParamField>

    <ParamField path="findAriaLabel" type="string" default="'Find text'">
      Accessible label for the find input. Also used as the floating surface label.
    </ParamField>

    <ParamField path="replacePlaceholder" type="string" default="'Replace'">
      Placeholder text for the replace input
    </ParamField>

    <ParamField path="replaceAriaLabel" type="string" default="'Replace text'">
      Accessible label for the replace input
    </ParamField>

    <ParamField path="noResultsLabel" type="string" default="'No results'">
      Text shown when the query has no matches
    </ParamField>

    <ParamField path="previousMatchLabel" type="string" default="'Previous match (Shift+Enter)'">
      Title/tooltip text for the previous-match button
    </ParamField>

    <ParamField path="previousMatchAriaLabel" type="string" default="'Previous match'">
      Accessible label for the previous-match button
    </ParamField>

    <ParamField path="nextMatchLabel" type="string" default="'Next match (Enter)'">
      Title/tooltip text for the next-match button
    </ParamField>

    <ParamField path="nextMatchAriaLabel" type="string" default="'Next match'">
      Accessible label for the next-match button
    </ParamField>

    <ParamField path="closeLabel" type="string" default="'Close (Escape)'">
      Title/tooltip text for the close button
    </ParamField>

    <ParamField path="closeAriaLabel" type="string" default="'Close find and replace'">
      Accessible label for the close button
    </ParamField>

    <ParamField path="replaceLabel" type="string" default="'Replace'">
      Replace-current button text
    </ParamField>

    <ParamField path="replaceAllLabel" type="string" default="'All'">
      Replace-all button text
    </ParamField>

    <ParamField path="toggleReplaceLabel" type="string" default="'Toggle replace'">
      Title/tooltip text for the show/hide replace-row button
    </ParamField>

    <ParamField path="toggleReplaceAriaLabel" type="string" default="'Toggle replace'">
      Accessible label for the show/hide replace-row button
    </ParamField>

    <ParamField path="matchCaseLabel" type="string" default="'Aa'">
      Text shown for the match-case toggle
    </ParamField>

    <ParamField path="matchCaseAriaLabel" type="string" default="'Match case'">
      Accessible label for the match-case toggle
    </ParamField>

    <ParamField path="ignoreDiacriticsLabel" type="string" default="'ä≡a'">
      Text shown for the ignore-diacritics toggle
    </ParamField>

    <ParamField path="ignoreDiacriticsAriaLabel" type="string" default="'Ignore diacritics'">
      Accessible label for the ignore-diacritics toggle
    </ParamField>
  </Expandable>

  <Expandable title="behavior + custom UI fields">
    <ParamField path="replaceEnabled" type="boolean" default="true">
      Whether replace actions are available. Set to `false` for a find-only popover.
    </ParamField>

    <ParamField path="component" type="Vue Component">
      Custom Vue component rendered inside the floating surface shell. Receives a `findReplace` prop. Mutually exclusive with `render`.
    </ParamField>

    <ParamField path="props" type="Record<string, unknown>">
      Extra props passed to the custom Vue component. Component-only; ignored for `render`.
    </ParamField>

    <ParamField path="render" type="(ctx: FindReplaceRenderContext) => { destroy?: () => void } | void">
      Framework-agnostic renderer for React or manual DOM mounting. Mutually exclusive with `component`.
    </ParamField>

    <ParamField path="resolver" type="(ctx: FindReplaceContext) => FindReplaceResolution | null | undefined">
      Runtime resolver for choosing built-in, custom, external, or suppressed rendering. Can coexist with `component`/`render`: the resolver runs first, and `null`/`undefined`/`{ type: 'default' }` falls through to the direct component/render or built-in UI.
    </ParamField>
  </Expandable>
</ParamField>

### Custom Vue component

Replace the built-in content with your own Vue component. Your component receives a `findReplace` prop with reactive state and actions:

```javascript theme={null}
import MyFindReplaceSurface from './MyFindReplaceSurface.vue';

modules: {
  surfaces: {
    findReplace: {
      component: MyFindReplaceSurface,
    },
  },
}
```

Inside your component:

```vue theme={null}
<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`:

```javascript theme={null}
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`:

```javascript theme={null}
modules: {
  surfaces: {
    findReplace: {
      replaceEnabled: false,
      resolver: (ctx) => {
        if (!ctx.replaceEnabled) {
          return { type: 'custom', component: ReadOnlyFindSurface };
        }
        return null; // fall through to built-in
      },
    },
  },
}
```

**Resolution types:**

| Type                                    | Behavior                                                       |
| --------------------------------------- | -------------------------------------------------------------- |
| `null` / `undefined`                    | Fall 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.

| Field                           | Vue `component` value        | External `render` value      | Description                                                                                                           |
| ------------------------------- | ---------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `findQuery`                     | `Ref<string>`                | `string` getter/setter       | Current search query                                                                                                  |
| `replaceText`                   | `Ref<string>`                | `string` getter/setter       | Current replacement text                                                                                              |
| `caseSensitive`                 | `Ref<boolean>`               | `boolean` getter/setter      | Match-case toggle                                                                                                     |
| `ignoreDiacritics`              | `Ref<boolean>`               | `boolean` getter/setter      | Ignore-diacritics toggle                                                                                              |
| `showReplace`                   | `Ref<boolean>`               | `boolean` getter/setter      | Whether the replace row is expanded                                                                                   |
| `matchCount`                    | `Ref<number>`                | `number` getter              | Total match count                                                                                                     |
| `activeMatchIndex`              | `Ref<number>`                | `number` getter              | Active match index, `-1` when none                                                                                    |
| `matchLabel`                    | `ComputedRef<string>`        | `string` getter              | Formatted label such as `3 of 12` or `No results`                                                                     |
| `hasMatches`                    | `ComputedRef<boolean>`       | `boolean` getter             | Whether there are any matches                                                                                         |
| `replaceEnabled`                | `boolean`                    | `boolean`                    | Whether replace actions are available                                                                                 |
| `texts`                         | `ResolvedFindReplaceTexts`   | `ResolvedFindReplaceTexts`   | All text strings resolved with defaults                                                                               |
| `goNext` / `goPrev`             | `() => void`                 | `() => void`                 | Navigate through matches                                                                                              |
| `replaceCurrent` / `replaceAll` | `() => void`                 | `() => void`                 | Run replacement actions                                                                                               |
| `registerFocusFn`               | `(fn) => void`               | `(fn) => void`               | Register 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) => void` | Close 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.

```javascript theme={null}
new SuperDoc({
  selector: '#editor',
  document: encryptedFile,
  modules: {
    surfaces: {
      passwordPrompt: true, // default: can omit
    },
  },
});
```

Set to `false` to disable:

```javascript theme={null}
modules: {
  surfaces: {
    passwordPrompt: false,
  },
}
```

### Text customization

Override any of the built-in copy:

```javascript theme={null}
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.',
    },
  },
}
```

<ParamField path="modules.surfaces.passwordPrompt" type="false | true | Object" default="true">
  Password prompt configuration. Enabled by default when omitted; set to `false` to disable.

  <Expandable title="text fields">
    <ParamField path="title" type="string" default="'Password Required'">
      Dialog heading on first attempt
    </ParamField>

    <ParamField path="invalidTitle" type="string" default="'Incorrect Password'">
      Dialog heading after a wrong password
    </ParamField>

    <ParamField path="description" type="string" default="'This document is password protected. Enter the password to open it.'">
      Explanatory text below the heading
    </ParamField>

    <ParamField path="placeholder" type="string" default="'Enter password'">
      Input placeholder text
    </ParamField>

    <ParamField path="inputAriaLabel" type="string" default="'Document password'">
      Accessible label for the password input
    </ParamField>

    <ParamField path="submitLabel" type="string" default="'Open'">
      Submit button text
    </ParamField>

    <ParamField path="cancelLabel" type="string" default="'Cancel'">
      Cancel button text
    </ParamField>

    <ParamField path="busyLabel" type="string" default="'Decrypting\u2026'">
      Submit button text while decrypting
    </ParamField>

    <ParamField path="invalidMessage" type="string" default="'Incorrect password. Please try again.'">
      Error message for wrong password
    </ParamField>

    <ParamField path="timeoutMessage" type="string" default="'Timed out while decrypting. Please try again.'">
      Error message for decryption timeout
    </ParamField>

    <ParamField path="genericErrorMessage" type="string" default="'Unable to decrypt this document.'">
      Error message for other decryption failures
    </ParamField>
  </Expandable>

  <Expandable title="custom UI fields">
    <ParamField path="component" type="Vue Component">
      Custom Vue component rendered inside the dialog shell. Receives a `passwordPrompt` prop (see handle reference below). Mutually exclusive with `render`.
    </ParamField>

    <ParamField path="props" type="Record<string, unknown>">
      Extra props passed to the custom Vue component. Component-only; ignored for `render`.
    </ParamField>

    <ParamField path="render" type="(ctx: PasswordPromptRenderContext) => { destroy?: () => void } | void">
      Framework-agnostic renderer for React or manual DOM mounting. Mutually exclusive with `component`.
    </ParamField>

    <ParamField path="resolver" type="(ctx: PasswordPromptContext) => PasswordPromptResolution | null | undefined">
      Conditional resolver for per-document customization. Can coexist with `component`/`render`: the resolver runs first, and `null`/`undefined`/`{ type: 'default' }` falls through to the direct component/render or built-in.
    </ParamField>
  </Expandable>
</ParamField>

### Custom Vue component

Replace the built-in dialog content with your own Vue component. Your component receives a `passwordPrompt` prop with everything it needs:

```javascript theme={null}
import MyPasswordDialog from './MyPasswordDialog.vue';

modules: {
  surfaces: {
    passwordPrompt: {
      component: MyPasswordDialog,
    },
  },
}
```

Inside your component:

```vue theme={null}
<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`:

```javascript theme={null}
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:

```javascript theme={null}
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:**

| Type                                    | Behavior                                         |
| --------------------------------------- | ------------------------------------------------ |
| `null` / `undefined`                    | Fall 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:

| Field             | Type                                                     | Description                                             |
| ----------------- | -------------------------------------------------------- | ------------------------------------------------------- |
| `documentId`      | `string`                                                 | The document ID requiring a password                    |
| `errorCode`       | `string`                                                 | `'DOCX_PASSWORD_REQUIRED'` or `'DOCX_PASSWORD_INVALID'` |
| `texts`           | `ResolvedPasswordPromptTexts`                            | All 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.

<Warning>
  Do not set `doc.password` or increment `editorMountNonce` directly. Use `passwordPrompt.attemptPassword(password)`: it handles the document mutation and remount internally.
</Warning>

### `{ 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](/editor/theming/custom-themes#surfaces).
