Skip to content

Link Popover

LinkPopover provides a floating URL input popover for creating and editing links. It listens for the linkEdit event (emitted by the Link mark’s Mod-K shortcut and toolbar button) and toggles a popover with a URL input, apply button, and remove button. It auto-prepends https:// for bare URLs and validates against allowed protocols.

This is a UI extension - it creates DOM elements for the popover. In headless setups, omit this extension and build your own link editing UI by listening for the linkEdit event directly.

Included in StarterKit by default.

Use LinkPopover when you need:

  • A floating UI for editing the href of a selected link
  • Cmd+K to open the popover on the current selection
  • A built-in URL validator that strips javascript: and vbscript: URLs

Skip it if:

  • You build a custom link-editing UI (toolbar input, modal, sidebar)
  • Your editor is read-only or links are managed entirely server-side

Select text and press Cmd+K / Ctrl+K to open the URL popover. Type a URL and press Enter to apply, or click the remove button to unlink.

The editor includes the Link mark and LinkPopover with the default theme. Click the link toolbar button or press Cmd+K / Ctrl+K to open the URL popover. Enter a URL, press Enter to apply. Click an existing link and press Cmd+K to edit or remove it.

Click to try it out

LinkPopover is included in StarterKit, so it works out of the box. To use it standalone:

import { Document, Paragraph, Text, Link, LinkPopover } from '@domternal/core';
import { DomternalEditor } from '@domternal/vanilla';
import '@domternal/theme';
const dm = new DomternalEditor(document.getElementById('editor')!, {
extensions: [Document, Paragraph, Text, Link, LinkPopover],
content: '<p>Select text and press <code>Cmd+K</code> / <code>Ctrl+K</code> to add a link.</p>',
});

To configure LinkPopover in StarterKit:

StarterKit.configure({
linkPopover: { protocols: ['http:', 'https:'] },
})

To disable LinkPopover in StarterKit:

StarterKit.configure({ linkPopover: false })
OptionTypeDefaultDescription
protocolsstring[]['http:', 'https:', 'mailto:', 'tel:']Allowed URL protocols for validation. Should match the Link mark’s protocols option.
import { LinkPopover } from '@domternal/core';
const editor = new Editor({
extensions: [
LinkPopover.configure({
protocols: ['http:', 'https:', 'mailto:'],
}),
],
});

LinkPopover does not register any commands. It uses the Link mark’s setLink and unsetLink commands internally.

LinkPopover does not register its own keyboard shortcuts. It is triggered by the Link mark’s Mod-K shortcut, which emits a linkEdit event.

The popover itself supports keyboard navigation:

KeyContextAction
EnterIn URL inputApply the link
EscapeAnywhere in popoverClose the popover and return focus to the editor
TabIn URL inputMove focus to the apply button
Shift-TabIn URL inputMove focus to the remove button (if visible) or apply button
Tab / Shift-TabOn buttonsCycle focus between input, apply, and remove
EnterOn a buttonClick the focused button

LinkPopover does not register any input rules.

LinkPopover does not register any toolbar items. The toolbar link button is registered by the Link mark with emitEvent: 'linkEdit', which triggers this popover.

LinkPopover listens for the linkEdit event on the editor. This event is emitted by:

  • The Link mark’s Mod-K keyboard shortcut
  • The Link toolbar button click (via emitEvent: 'linkEdit')

Each linkEdit event toggles the popover: if closed, it opens; if open, it closes and returns focus to the editor.

When the popover opens, it:

  1. Detects existing links - checks if the cursor or selection is inside a link mark. If so, pre-fills the input with the existing href and shows the remove button.
  2. Shows a selection decoration - when text is selected, the browser hides the native selection highlight when the input takes focus. The plugin renders an inline dm-link-pending decoration to keep the selected text visually highlighted.
  3. Positions with Floating UI - uses positionFloating() (which wraps @floating-ui/dom) to position the popover below the anchor element (toolbar button) or below the cursor position.
  4. Focuses the input - auto-focuses and selects the URL input content.

When the user presses Enter or clicks the apply button:

  1. Ignores an empty input - if the input is blank after trimming, the popover just closes and focus returns to the editor. An existing link is left untouched; removing a link is done with the remove button (unsetLink()) only.
  2. Auto-prepends protocol - if the URL has no protocol (e.g., example.com), https:// is prepended automatically.
  3. Validates the URL - checks the URL against the configured protocols list using isValidUrl(). Invalid URLs are silently rejected and the popover closes.
  4. Handles existing links - if the cursor is inside an existing link with no selection, the plugin selects the full link range using getMarkRange() and updates the mark in a single transaction to avoid a visual flash.
  5. Applies the mark - calls editor.commands.setLink({ href }) to set the link.
  6. Closes and refocuses - hides the popover and returns focus to the editor.

The remove button calls editor.commands.unsetLink(), closes the popover, and returns focus to the editor. It is only visible when the cursor is inside an existing link.

Clicking outside the popover closes it. Clicks on the toggle anchor element (the toolbar button that opened the popover) are ignored so the button’s own click handler can toggle correctly.

Where the popover lives in the DOM depends on how it was opened:

  • Opened from a button (toolbar or bubble menu, which pass an anchorElement with the linkEdit event): the popover is positioned against that button (placement: 'bottom-start'). If the button sits inside a .dm-editor wrapper, the popover is reparented there so it inherits the editor’s --dm-* custom properties. Bubble menu buttons qualify, since the bubble menu element is moved into .dm-editor when the plugin starts. A toolbar rendered next to the editor, the layout the framework guides use, does not, so the popover stays in document.body.
  • Opened from the Mod-K shortcut (no anchor): the popover stays in document.body, positioned below the cursor (placement: 'bottom'), so it is never clipped by overflow: hidden on the editor wrapper.

On every open the editor’s dm-theme-* class is copied onto the popover (copyThemeClass), which is what keeps dark-mode tokens resolving on the document.body path and lets runtime theme toggles propagate.

<div class="dm-link-popover" data-dm-editor-ui>
<input class="dm-link-popover-input" type="url" placeholder="Enter URL..." aria-label="URL" />
<button class="dm-link-popover-btn dm-link-popover-apply" aria-label="Apply link">
<!-- check icon -->
</button>
<button class="dm-link-popover-btn dm-link-popover-remove" aria-label="Remove link">
<!-- linkBreak icon -->
</button>
</div>

Icons used: check (apply) and linkBreak (remove) from defaultIcons.

While the popover is open with a text selection, the plugin maintains an inline decoration with the class dm-link-pending over the selected range. This paints a 12% tint of --dm-accent over the range (color-mix(in srgb, var(--dm-accent) 12%, transparent)), so the highlight follows a customized accent instead of a fixed blue. At the default --dm-accent of #2563eb it resolves to a light blue.

LinkPopover writes its open state to editor.storage.link.isOpen. This allows the Link toolbar button to show an “expanded” state while the popover is open.

The @domternal/theme package includes full styles for the link popover with light and dark theme support.

ClassDescription
.dm-link-popoverMain popover container (fixed position, z-index 60)
.dm-link-popover-inputURL text input (min-width 14rem)
.dm-link-popover-btnShared button styles (1.5rem square)
.dm-link-popover-applyApply button (blue on hover)
.dm-link-popover-removeRemove button (red on hover)
.dm-link-pendingInline decoration on selected text while popover is open
import { LinkPopover } from '@domternal/core';
import type { LinkPopoverOptions } from '@domternal/core';
ExportTypeDescription
LinkPopoverExtensionThe link popover extension
LinkPopoverOptionsTypeScript typeOptions for LinkPopover.configure()

@domternal/core - LinkPopover.ts

  • Link - hyperlinks with URL validation and paste handling
  • Bubble Menu - floating menu shown on text selection
  • Security - XSS protection and content sanitization