Scenario

Link as non-anchor markup during editing (e.g. span + data-href)

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.

formatting
Scenario ID
scenario-link-span-internal-representation

Details

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.

Problem Overview

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.

Observed patterns

ApproachTypical DOM while editingNotes
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"> wrappersUsed for widgets; different trade-offs than span+link.

Impact

  • Accessibility: A live span is not a link in the accessibility tree until you expose role/name or export to <a>.
  • Clipboard: Copy may produce HTML without native link semantics unless you customize copy / serialize.
  • Paste: Pasted <a> may need normalization into your internal representation.
  • Maintenance: You own URL validation, rel, target, and security (javascript: URLs, etc.).

When teams choose span (or non-<a>) internally

  • Strong control over caret placement at link boundaries without browser default navigation.
  • Avoiding invalid nested <a> during partial edits before merge/sanitization.
  • Matching a CMS or design tool pipeline that already uses data-* for internal references.

Browser comparison

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).

Solutions

  • Serialize at boundaries: On blur, save, or publish, convert internal spans to <a href> (or Markdown) with tests.
  • Optional decoration: role="link" + tabIndex={0} on span only if you intentionally mirror link semantics for a11y while editing (still not the same as native navigation).
  • Reuse library defaults: If you do not need span semantics, Lexical / Tiptap / ProseMirror defaults are battle-tested <a>-based paths.

References (external)

Mainstream editors: default is <a>

  • Lexical@lexical/link: LinkNode / AutoLinkNode build a link element (<a>) in the DOM (createDOM).
  • TiptapLink extension: mark that renders as <a> with configurable HTMLAttributes.
  • SlateInlines example: inline links are typically rendered as <a {...attributes}> in renderElement.
  • ProseMirror — link marks in schema usually toDOM to ["a", attrs, 0] (see also this repo’s model-schema link).

Discussions of editing difficulty and workarounds

Scenario flow

Visual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.

React Flow mini map

Variants

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

Cases

Open a case to see the detailed description and its dedicated playground.

Related Scenarios

Other scenarios that share similar tags or category.

Tags: link

Link clicks interfere with contenteditable editing

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.

3 cases
Tags: link

Link insertion and editing behavior varies across browsers

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.

3 cases
Tags: link

Typing adjacent to formatted elements causes unexpected behavior

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.

1 case

Comments & Discussion

Have questions, suggestions, or want to share your experience? Join the discussion below.