Definition
This is knowledge you use when you want to show a drag-handle icon (like ⣿) or decorative character on screen, but want to prevent that decorative character from being copied along when the user copies the body text.
Two background concepts: Selection.toString() (a browser API that extracts the current selection as a string) and serialization at copy time (the process of converting a selection into clipboard text) both build the string based on the DOM's text nodes (the actual character data inside the HTML). So user-select: none (the CSS that blocks mouse-drag selection), commonly used to block this, only makes it harder for the user to newly drag-select, and does not guarantee that character is fully excluded from the copy result of an already-made selection. To exclude it for sure, it's safer to place that character not in a text node but in CSS ::before / ::after content (generated content — fake content that isn't in the HTML and is only drawn in via CSS).
Why it's needed
Problem situation: if you put a block editor's drag handles, decorative icons, or hidden labels into the DOM as real text, then when the user selects and copies that block, the handle glyph (an icon character like ⣿) mixes with the body text and goes into the clipboard. On paste it comes out messy, like "⣿ actual content."
Here user-select: none alone isn't enough, and draggable={false} (an attribute that makes an element non-draggable) only blocks the drag action and is unrelated to the copy string. So you need an approach that separates "the decoration visible on screen" from "the meaningful text that gets copied."
How it works
- When the user drags a range,
Selectionpoints to DOM text nodes. - On copy, the browser serializes the text of the selection range. The number of block-boundary newlines (
\nvs\n\n) can differ per path·browser. user-select: nonemakes it harder for a pointer to include that node in the selection range, but it can still be included via parent selection·programmatic selection·some serialization paths.::before { content: "…" }is generated content, so it isn't a DOM text node and is generally not included in the usualSelection.toString()/ clipboard paths.
| Item | Description | Caveat |
|---|---|---|
user-select: none | Makes pointer selection hard | Not a guarantee of full Selection/copy exclusion |
::before content | Generated content, not DOM text | a11y alternative text is a separate design |
draggable=false | Suppresses native element drag | For blocking img/a default behavior |
| Clipboard serialization | Newlines differ per browser·path | Normalize tests to non-empty lines |
Practical application
Move glyphs that must be excluded from selection/copy into CSS generated content.
Block unintended dragging of media·links with draggable={false}. Normalize clipboard E2E to tolerate per-browser newline differences by comparing only non-empty lines.
If a screen reader needs to know the handle's meaning, provide it separately via the accessibility tree with aria-label etc. Generated content alone does not solve a11y.
Trade-offs
- Pseudo content reduces copy pollution, but some browsers/tools may treat generated content differently. Verify the critical path on real devices.
- Removing decoration from the DOM makes style·hit-area design depend more on CSS.
When not to use
- When that character is meaningful body text — it should be copied, so keep it as a text node.
- UX where the decoration must be selectable (e.g. selecting the handle itself to manipulate it) — use a different interaction model.
Common mistakes
- Assuming "it's excluded from the clipboard" with
user-select: nonealone. - Leaving a handle glyph as a text node and letting it mix into copy results.
- Comparing clipboard tests strictly down to the newline count, causing flakes across browsers.
- Ignoring a11y and moving only the visual glyph to a pseudo-element.
Related concepts
- html5-drag-and-drop — covered together with the problem of handles·images·links intercepting drag/selection.