Copy a sentence from a web article and paste it into an email draft: it keeps its bold, italic, link, and even font colour. Paste the same sentence into a Slack message: it loses everything and comes in as plain text. Paste it into your own rich-text editor: you get some mixture of the two, and nothing you do seems to reliably control which. Users file bug reports. What is going on?
The mechanism is not magic, and it is not under the browser's sole control. This article traces the full path from "user presses Ctrl+C" to "paste target shows styled content or doesn't", and explains the design decisions each paste destination makes.
Every copy produces multiple representations
When you copy a selection in a web page, the browser does not hand the text over as a single string. It serialises the selection into several MIME-typed representations at once — typically text/plain (unformatted text) and text/html (an HTML fragment preserving tags, styles, and links). Other sources add more: a Word document produces Office-flavoured HTML plus a proprietary binary format; Google Docs adds a application/x-vnd.google-docs-document-slice-clip+wrapped format to enable lossless copy-paste back into Docs.
You can see this for yourself: copy something, then paste into Clipboard Inspector. Every available representation appears as a separate card. This is the raw material every paste target is working from.
The paste destination chooses which representation to use
Here is the key insight: formatting is not a property of the clipboard. It is a choice made by the receiving application. A plain-text destination asks the clipboard for text/plain, receives an unformatted string, and inserts that. A rich-text destination asks for text/html, receives the HTML fragment, and inserts it (possibly after running it through a sanitiser). Some destinations offer the user a choice: "Paste" versus "Paste and Match Style" in macOS, Ctrl+Shift+V versus Ctrl+V in many Linux applications.
So when Slack drops formatting, it is not because the formatting was stripped somewhere; it is because Slack's paste handler chose to read text/plain, ignoring the text/html that was available all along. Slack made that choice deliberately — consistent, predictable formatting across a team chat is worth more than preserving the stylistic choices of every source web page.
Rich-text editors are the hardest case
Editors such as Gmail's composer, Notion, Google Docs itself, and every CMS rich-text widget want to accept some formatting (users expect bold to paste as bold) but not all of it (users do not want Times New Roman 12pt with a specific color: #1a1a1a showing up on top of the editor's house typography).
Each editor ships a paste pipeline that typically does these steps:
- Read
text/htmlfrom the clipboard. If there is none, readtext/plainand skip the rest. - Parse the HTML into a DOM fragment. Parsing in a detached
DocumentFragmentavoids accidentally evaluating scripts or firing resource loads. - Sanitise. Remove scripts, event handlers, dangerous URLs (
javascript:), and any tag not on the allow-list. - Normalise. Flatten
<font>tags, stripclassand moststyleattributes, collapse repeated whitespace, and convert Office-specific constructs (paragraph numbering, list styles) to semantic equivalents. - Map to internal schema. Most modern editors have an internal document model that is not HTML. ProseMirror has a schema; Slate has a value tree; TipTap builds on ProseMirror. The sanitised HTML is converted into the editor's own node types.
- Insert at the selection. Finally, the pipeline replaces the current selection with the mapped fragment, preserving the editor's ability to undo the paste as a single operation.
Each step is where things go wrong.
Common problems and their root causes
1. Pasted Word documents carry a mountain of mso- styles
Microsoft Office serialises clipboard HTML with inline mso-* CSS variables and a "StartFragment"/"EndFragment" comment scaffold. Without aggressive normalisation, these styles override the host document's fonts, margins, and list styling. Fix: strip mso-* properties from every style attribute, then strip the attribute entirely if nothing remains.
2. Google Docs paragraphs come with unusable structure
Google Docs exports copied paragraphs inside <b style="font-weight:normal"> wrappers that have no semantic meaning, plus <span>s with granular style attributes. A straight innerHTML insertion produces bizarre DOM structures that confuse spellchecking and accessibility tooling. Fix: unwrap non-semantic wrappers and collapse adjacent spans with identical effective styles.
3. Newlines disappear or duplicate
HTML's whitespace rules collapse consecutive spaces and newlines. Sources differ: Word produces <p> per paragraph; Google Docs produces <p> with explicit non-breaking spaces; plain-text sources produce actual \n characters in text/plain. Fix: if inserting into an HTML editor, prefer text/html and do not substitute with text/plain's newlines. If inserting into a plain-text field, always use text/plain, which already encodes line breaks as characters.
4. Links silently disappear
Source HTML may use <a href="..."> or attach the link via inline CSS (text-decoration: underline; color: blue). Over-aggressive style stripping removes the visual cue but leaves the <a> tag — or worse, strips the tag too. Fix: always preserve href on anchors; only strip inline colour/underline when the anchor is kept and the editor applies its own link styling.
5. Images come as data URIs or remote URLs
Some sources (screenshots, Google Docs) embed images as data: URIs in the pasted HTML. Others (a browser copying a rendered <img>) reference the original HTTP URL. Your pipeline must decide: upload the data URI to your media server immediately, or keep the external URL (which will 404 the day the source page disappears). Most editors upload everything immediately and rewrite the src attribute on success.
6. Lists break
Office and Docs do not use <ul>/<ol>/<li> in the way HTML was designed to. Word outputs paragraphs prefixed with a bullet glyph and a tab. Fix: detect paragraphs that begin with list markers (bullets, numbered prefixes, indentation patterns) and re-build them as real lists in your output.
7. Tables explode
Spreadsheet copies produce <table>-based HTML with inline borders, column-spanning hacks, and occasional nested tables. Most editors choose to strip tables on paste unless they are explicitly designed to host them.
"Paste and Match Style" — what really happens
Many operating systems and applications provide a "Paste and Match Style" command bound to Cmd+Shift+V (macOS), Ctrl+Shift+V (Windows/Linux), or a menu entry. When invoked, the paste handler reads text/plain only, ignoring text/html. The result is identical to copying from a plain-text editor in the first place. There is no clever "remove-only-the-offending-styles" transformation — it is the same conservative choice Slack makes by default, exposed as an option.
Implementing your own "paste as plain text"
In a rich-text editor, the simplest implementation is:
document.addEventListener('paste', (e) => {
if (!e.shiftKey) return; // normal paste path
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
insertPlainTextAtSelection(text);
});
Detect the Shift modifier on the paste event and branch: with Shift, read plain text; without Shift, run the full HTML pipeline. Users accustomed to native "match style" behaviour will feel at home.
When to prefer plain text outright
If your editor does not benefit from inbound formatting (a one-line search field, a tag input, a code-comment input), read text/plain always and save yourself the sanitiser. The UX is more predictable and the attack surface is smaller.
Testing paste behaviour
Build a test harness that seeds the clipboard with representative fixtures — HTML from Word, HTML from Docs, HTML from a rendered web page, a plain-text fixture, a mixed fixture — and asserts on the resulting editor state. Playwright's clipboard methods are ideal for this. A good test suite runs in under a second and catches the category of bug that used to take a week to diagnose.
Inspecting real pastes
When a user reports "the formatting looks wrong", ask them to reproduce the exact copy, then paste it into Clipboard Inspector and send you the resulting data. The difference between what the clipboard contained and what your editor produced is the bug. Fix the pipeline step that dropped or mangled the representation and re-run the test.
Paste formatting feels mysterious because it depends on many layers cooperating — source app, OS clipboard, browser, editor — but each layer is observable. Observe them one at a time and the "mystery" reduces to a short list of fixable decisions.