Pattern — span + data-href for links during edit, <a> on export
OS: Any Any · Device: Desktop or Laptop Any · Browser: Any Any · Keyboard: US
Open case →Scenario
Some editors keep URLs on a span (or other non-anchor inline) with data-* attributes while the user edits, then serialize to semantic <a href> for publish or clipboard. Mainstream framework defaults still use <a> in the live DOM; span-based linking is a deliberate product/engineering choice with distinct trade-offs.
Some editors keep URLs on a span (or other non-anchor inline) with data-* attributes while the user edits, then serialize to semantic <a href> for publish or clipboard. Mainstream framework defaults still use <a> in the live DOM; span-based linking is a deliberate product/engineering choice with distinct trade-offs.
Native <a> inside contenteditable participates in navigation, default click behavior, and selection quirks (link click / editing). Teams that want “click places caret, Ctrl+click opens URL” or that fight nested-anchor normalization sometimes avoid <a> until export.
| Approach | Typical DOM while editing | Notes |
|---|---|---|
| Default (most libraries) | <a href="…">text</a> | Lexical LinkNode, Tiptap Link, ProseMirror link mark, Slate examples — see references. |
| Internal span | <span data-href="…" class="…">text</span> (or similar) | URL stored on data attribute; click/keydown handled in JS; export pipeline emits <a>. |
| Hybrid | <a contenteditable="false"> wrappers | Used for widgets; different trade-offs than span+link. |
span is not a link in the accessibility tree until you expose role/name or export to <a>.copy / serialize.<a> may need normalization into your internal representation.rel, target, and security (javascript: URLs, etc.).<a>) internally<a> during partial edits before merge/sanitization.data-* for internal references.This is less “browser A vs B” than your DOM contract vs native editing. The same browser behaves differently if the node is <a> vs <span> (e.g. closest('a'), default link styling).
<a href> (or Markdown) with tests.role="link" + tabIndex={0} on span only if you intentionally mirror link semantics for a11y while editing (still not the same as native navigation).<a>-based paths.<a>@lexical/link: LinkNode / AutoLinkNode build a link element (<a>) in the DOM (createDOM).<a> with configurable HTMLAttributes.<a {...attributes}> in renderElement.toDOM to ["a", attrs, 0] (see also this repo’s model-schema link).data-href subsectionVisual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.
Each row is a concrete case for this scenario, with a dedicated document and playground.
| Case | OS | Device | Browser | Keyboard | Status |
|---|---|---|---|---|---|
| ce-0587-link-span-data-href-editing-pattern | Any Any | Desktop or Laptop Any | Any Any | US | draft |
Open a case to see the detailed description and its dedicated playground.
OS: Any Any · Device: Desktop or Laptop Any · Browser: Any Any · Keyboard: US
Open case →Other scenarios that share similar tags or category.
After deleting an empty or inline element (e.g. span, b) inside contenteditable, typing causes the browser to recreate the deleted element, leading to unpredictable DOM and editor state.
When a link is inside a contenteditable element, clicking on the link may navigate away or trigger unexpected behavior instead of allowing text editing. The behavior varies across browsers and can make it difficult to edit link text or select links for deletion.
When inserting or editing links in contenteditable elements, the behavior varies significantly across browsers. Creating links, editing link text, and removing links can result in unexpected DOM structures or lost formatting.
Repeated bold/italic toggles or browser execCommand can nest spans deeply—serialization, selection, and IME boundaries degrade.
When typing text next to formatted elements (links, bold, italic, etc.) in contenteditable, the input events may include the formatted element's text in event.data, selection ranges may include the formatted element, and text may be inserted into the formatted element instead of after it. This occurs across different browsers and input methods.
Have questions, suggestions, or want to share your experience? Join the discussion below.