Built-in UI

Links and context menus

Configure link behavior and add application actions to the built-in Editor context menu.

The built-in UI can create and edit hyperlinks, show a link popover, and combine SuperDoc's default context menu with application-owned actions.

Configure both interaction surfaces

This example keeps the default menu, adds one selection-aware action, and replaces the clicked-link popover with framework-neutral DOM:

<div id="toolbar"></div>
<div id="editor"></div>

<script type="module" src="/src/main.ts"></script>
import { SuperDoc, type ContextMenuConfig, type LinkPopoverResolver } from 'superdoc';
import 'superdoc/style.css';

const resolveLinkPopover: LinkPopoverResolver = ({ href }) => ({
  type: 'external',
  render: ({ container, closePopover }) => {
    const link = document.createElement('a');
    link.href = href;
    link.target = '_blank';
    link.rel = 'noopener noreferrer';
    link.textContent = 'Open link';

    const close = document.createElement('button');
    close.type = 'button';
    close.textContent = 'Close';
    close.addEventListener('click', closePopover);
    container.append(link, close);

    return { destroy: () => close.removeEventListener('click', closePopover) };
  },
});

const contextMenu = {
  includeDefaultItems: true,
  customItems: [
    {
      id: 'application-actions',
      items: [
        {
          id: 'copy-selection-to-workflow',
          label: 'Copy selection to workflow',
          showWhen: ({ hasSelection }) => hasSelection,
          // `onSelect`, not the v1 `action` callback: v2 cannot invoke `action`
          // because its first argument is a ProseMirror Editor this runtime does
          // not have, so an `action`-only item warns once and dismisses.
          //
          // `context` is the snapshot captured when the menu opened, and it is
          // null when none was captured. `selectedText` is read synchronously to
          // keep the click's user activation, which `navigator.clipboard`
          // requires; it is empty when a worker-backed read had not settled by
          // click time, so copy only when it carries text. Awaiting
          // `selectedTextSettled` would return the accurate text but spend the
          // activation the clipboard write needs.
          // `onSelect` returns `void | Promise<void>`, so returning the write
          // hands the rejection to the runtime instead of leaving an unhandled
          // one when clipboard access is denied. The write is still initiated
          // synchronously inside the gesture, which is what the permission
          // check requires.
          onSelect: ({ context }) => {
            if (!context?.selectedText) return;
            return navigator.clipboard.writeText(context.selectedText);
          },
        },
      ],
    },
  ],
} satisfies ContextMenuConfig;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  ui: {
    // A toolbar renders only once it has somewhere to mount. Naming groups
    // without a container leaves the handle available and the toolbar absent.
    toolbar: { container: '#toolbar', groups: { center: ['link'] } },
    contextMenu,
  },
  // `popoverResolver` has no `ui` equivalent yet: the link popover reads it
  // from `modules.links`, so this one stays where the runtime looks for it.
  modules: {
    links: { popoverResolver: resolveLinkPopover },
  },
});

window.addEventListener('beforeunload', () => superdoc.destroy());

modules.links.popoverResolver runs synchronously when a link is clicked. Return default to use SuperDoc's popover, none to suppress it, or external to render with your own framework or DOM. The external renderer must return a cleanup function when it installs listeners or mounts a framework root. This resolver is the one built-in UI setting that has no ui equivalent yet, so it stays under modules.links; ui: { linkPopover: false } suppresses the popover entirely.

ui.contextMenu.customItems adds sections to both the built-in right-click menu and the slash menu. SuperDoc calls showWhen() with the current context; return true when the action is relevant. Keep application actions narrow and avoid reconstructing document positions from selectionStart or selectionEnd; use the Document API and public selection targets for document work.

Separate navigation from mutation

Opening a link is browser navigation. Creating, changing, and removing hyperlinks are document mutations. The built-in toolbar handles those mutations against the live selection and respects viewing mode. For programmatic hyperlink changes, use the generated Document API hyperlink operations instead of simulating toolbar clicks.

Context-menu visibility is not authorization. Server-side access and collaboration policy remain the application's responsibility.

On this page