Skip to main content
Control what happens when a user clicks a link in the editor. By default, SuperDoc shows a built-in popover with the link URL and edit controls. With popoverResolver, you can replace it with your own UI in any framework.

Quick start

No configuration needed for the default behavior — click a link and the built-in popover appears. To customize, add a popoverResolver to the links module:
new SuperDoc({
  selector: '#editor',
  document: file,
  modules: {
    links: {
      popoverResolver: (ctx) => {
        // Navigate anchor links instead of showing a popover
        if (ctx.isAnchorLink) {
          window.location.hash = ctx.href;
          return { type: 'none' };
        }
        // Everything else gets the default popover
        return { type: 'default' };
      }
    }
  }
});

Configuration

Synchronous function called when a user clicks a link. Receives a context object and returns a resolution that determines which popover to show. Return null or undefined to use the default popover.
The resolver must be synchronous. Do not return a Promise. If the resolver throws, SuperDoc falls back to the default popover and calls onException.

Resolver context

The resolver receives a LinkPopoverContext object with all information about the clicked link:
PropertyTypeDescription
editorEditorThe editor instance
hrefstringThe href attribute of the clicked link
targetstring | nullThe target attribute
relstring | nullThe rel attribute
tooltipstring | nullThe title attribute
elementHTMLAnchorElementThe clicked anchor DOM element
clientXnumberX coordinate of the click
clientYnumberY coordinate of the click
isAnchorLinkbooleantrue when href starts with #
documentModestringCurrent mode: 'editing', 'viewing', or 'suggesting'
position{ left: string, top: string }Computed popover position relative to the editor surface
closePopover() => voidClose the popover programmatically

Resolution types

The resolver returns one of four resolution types. Return null or undefined to use the default popover.

default

Show the built-in link popover with URL display and edit controls.
popoverResolver: (ctx) => {
  return { type: 'default' };
}

none

Suppress the popover entirely. Use this when the resolver handles the click itself (navigation, opening a modal, logging, etc.).
popoverResolver: (ctx) => {
  // Open external links in a new tab, no popover
  if (!ctx.isAnchorLink) {
    window.open(ctx.href, '_blank');
    return { type: 'none' };
  }
  return { type: 'default' };
}

custom

Render a Vue component inside the built-in popover shell. editor and closePopover are automatically injected as props alongside any props you provide.
import MyLinkPopover from './MyLinkPopover.vue';

popoverResolver: (ctx) => {
  return {
    type: 'custom',
    component: MyLinkPopover,
    props: { href: ctx.href }
  };
}
Your component receives editor, closePopover, and any additional props:
<script setup>
defineProps({
  editor: { type: Object, required: true },   // auto-injected
  closePopover: { type: Function, required: true }, // auto-injected
  href: { type: String },                     // your custom prop
});
</script>

external

Mount framework-agnostic UI into a raw DOM container. Use this for React, Svelte, vanilla JS, or any non-Vue framework. SuperDoc creates a positioned <div> element and passes it to your render function. You mount your UI into that container. Return a { destroy } callback for cleanup when the popover closes.
popoverResolver: (ctx) => {
  return {
    type: 'external',
    render: ({ container, closePopover, editor, href }) => {
      // Mount your UI into the container
      container.innerHTML = `
        <a href="${href}" target="_blank">Open link</a>
        <button>Close</button>
      `;
      container.querySelector('button').onclick = closePopover;

      return {
        destroy: () => {
          // Clean up event listeners, unmount frameworks, etc.
        }
      };
    }
  };
}
The render function receives an ExternalPopoverRenderContext:
PropertyTypeDescription
containerHTMLElementEmpty positioned DOM container to mount your UI into
closePopover() => voidClose the popover, call destroy, and return focus to the editor
editorEditorThe editor instance
hrefstringThe href of the clicked link
The popover automatically closes on click-outside and Escape key — matching the built-in popover behavior. Your destroy callback is called in both cases.

Framework examples

Use createRoot to mount a React component into the external container. Return destroy to unmount cleanly.
import { createRoot } from 'react-dom/client';
import { LinkPreview } from './LinkPreview';

new SuperDoc({
  selector: '#editor',
  document: file,
  modules: {
    links: {
      popoverResolver: (ctx) => ({
        type: 'external',
        render: ({ container, closePopover, href }) => {
          const root = createRoot(container);
          root.render(
            <LinkPreview href={href} onClose={closePopover} />
          );
          return { destroy: () => root.unmount() };
        }
      })
    }
  }
});
With the React wrapper:
import { SuperDocEditor } from '@superdoc-dev/react';
import { createRoot } from 'react-dom/client';
import { LinkPreview } from './LinkPreview';

function App() {
  return (
    <SuperDocEditor
      document={file}
      documentMode="editing"
      modules={{
        links: {
          popoverResolver: (ctx) => ({
            type: 'external',
            render: ({ container, closePopover, href }) => {
              const root = createRoot(container);
              root.render(
                <LinkPreview href={href} onClose={closePopover} />
              );
              return { destroy: () => root.unmount() };
            }
          })
        }
      }}
    />
  );
}

Styling

External popovers use CSS custom properties with sensible defaults that match the built-in popover. Override them to match your design system.

Shared popover variables

These apply to both the built-in popover and external link popovers:
VariableDefaultDescription
--sd-popover-bgwhiteBackground color
--sd-popover-z-index1000Stack order
--sd-popover-radius6pxBorder radius
--sd-popover-shadow0 0 0 1px rgba(0,0,0,0.05), 0px 10px 20px rgba(0,0,0,0.1)Box shadow
--sd-popover-min-width120pxMinimum width
--sd-popover-min-height40pxMinimum height
Override just the external link popover without affecting other popovers:
VariableFallbackDescription
--sd-external-link-popover-bg--sd-popover-bgBackground color
--sd-external-link-popover-z-index--sd-popover-z-indexStack order
--sd-external-link-popover-radius--sd-popover-radiusBorder radius
--sd-external-link-popover-shadow--sd-popover-shadowBox shadow
--sd-external-link-popover-min-width--sd-popover-min-widthMinimum width
--sd-external-link-popover-min-height--sd-popover-min-heightMinimum height
Example — dark theme for external link popovers:
.superdoc-root {
  --sd-external-link-popover-bg: #1a1a2e;
  --sd-external-link-popover-radius: 10px;
  --sd-external-link-popover-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
}
The external popover container also has the class sd-external-link-popover for direct CSS targeting:
.sd-external-link-popover {
  font-family: inherit;
  color: #333;
}

Behavior

  • Toggle off: Clicking a link while its popover is already open closes the popover.
  • Click outside: Clicking anywhere outside the popover closes it.
  • Escape key: Pressing Escape closes the popover.
  • Focus: When a popover closes, focus returns to the editor.
  • Error handling: If the resolver or render function throws, SuperDoc falls back to the default popover and calls the onException callback.
  • Cursor: The editor cursor moves to the clicked link position before the resolver runs.

Conditional resolution

Use resolver context to show different popovers based on link type, document mode, or any other condition:
popoverResolver: (ctx) => {
  // Anchor links — navigate without a popover
  if (ctx.isAnchorLink) {
    document.getElementById(ctx.href.slice(1))?.scrollIntoView();
    return { type: 'none' };
  }

  // Viewing mode — open links directly
  if (ctx.documentMode === 'viewing') {
    window.open(ctx.href, '_blank');
    return { type: 'none' };
  }

  // Internal links — custom component
  if (ctx.href.startsWith('https://internal.app/')) {
    return {
      type: 'custom',
      component: InternalLinkPopover,
      props: { href: ctx.href }
    };
  }

  // Everything else — default popover
  return { type: 'default' };
}