event.preventDefault() is the simplest API in the whole paste-event pipeline and the most misused. Call it too eagerly and your users find that plain text no longer pastes into text fields. Miss it in the one place it is needed and your rich-text editor ends up with VML metadata from Word. This article lays out the rules: when to call preventDefault, when to not call it, and the mental model that tells you which is which for any new paste handler you write.
What preventDefault actually does in a paste handler
The default action of a paste event is "insert the pasted content into the current editable target using the browser's built-in logic". For a plain <input> or <textarea>, the built-in logic inserts text/plain. For a contenteditable element, the built-in logic inserts text/html if available, falling back to text/plain.
preventDefault() cancels that built-in insertion. It does not stop the event from bubbling, does not unset clipboardData, and does not affect whether your handler runs. It simply tells the browser "do not insert anything — I will take it from here".
Rule 1: only call preventDefault if you're going to insert something
The canonical bug: a paste handler calls preventDefault() unconditionally, then conditionally inserts only specific types of content. If the user pastes a type the handler does not care about, nothing lands in the input because the browser's default was cancelled.
The fix is to call preventDefault() only inside the branch that does the custom insertion:
element.addEventListener('paste', (e) => {
const html = e.clipboardData.getData('text/html');
if (html && containsMagicMarker(html)) {
e.preventDefault();
insertCustom(html);
}
// otherwise let the browser handle it
});
Rule 2: rich-text editors must always call preventDefault
A rich-text editor cannot afford the browser's default for contenteditable. The default insertion preserves source styles, which is exactly what the editor spent effort normalising out. In an editor, the flow is:
- Listen for paste.
- Call
preventDefault()immediately. - Read the clipboard, sanitise, and insert through the editor's own API.
Even if the sanitiser fails or the user pasted plain text, the editor should insert the value itself rather than fall back to the browser default. This guarantees that all insertions go through the same code path — critical for consistent behaviour and for undo/redo to work properly.
Rule 3: plain text inputs usually should not call preventDefault
For a <textarea> or plain <input>, the browser's default is already what you want: insert the text/plain value. Calling preventDefault here means you have to reimplement selection-aware insertion, cursor positioning, and undo-stack integration — all of which the browser does correctly for free.
The exception: when you want to transform the pasted value before it lands. Lowercasing, trimming whitespace, or parsing an identifier from a URL all require preventDefault and a manual insert:
input.addEventListener('paste', (e) => {
const raw = e.clipboardData.getData('text/plain');
const transformed = normalise(raw);
if (transformed !== raw) {
e.preventDefault();
insertAtCursor(input, transformed);
}
});
Rule 4: for file and image pastes, preventDefault only if you're handling the files
When a user pastes a screenshot into a <textarea>, the browser's default is to insert the filename as text — rarely useful. For a handler that actually processes the pasted image (uploads it, previews it), call preventDefault in the files branch:
textarea.addEventListener('paste', (e) => {
const files = [...e.clipboardData.files].filter(f => f.type.startsWith('image/'));
if (files.length) {
e.preventDefault();
for (const file of files) uploadImage(file);
}
});
Rule 5: do not call preventDefault on copy unless you also write to the clipboard
A copy event has the same rule: preventDefault cancels the browser's default behaviour, which is "put the selected content on the clipboard". If you cancel and do not write anything yourself, the user copies nothing. Use event.clipboardData.setData to write the custom value, then call preventDefault to stop the default:
element.addEventListener('copy', (e) => {
e.preventDefault();
e.clipboardData.setData('text/plain', customValue);
e.clipboardData.setData('text/html', customHtml);
});
The mental model
Think of preventDefault as saying "I am taking full responsibility for what happens next". Only say that when you actually are taking responsibility. If any code path leaves the event handler without inserting the value, do not call preventDefault on that path.
A useful test: comment out every line in your handler except preventDefault, then try pasting various content. If any kind of paste leaves the input empty, you are cancelling without a replacement. Uncomment the handler and pair each preventDefault with the line that inserts the value.
Common bugs this avoids
- "Paste does nothing sometimes." Usually means
preventDefaultis called unconditionally but the insertion branch has an early return. - "Undo does not work after paste." Usually means the handler inserted through
innerHTMLor direct DOM mutation instead of through the editor's API. The editor's undo stack does not know about the change. - "Cursor is in the wrong place after paste." Manual insertion usually sets the cursor to the end of the inserted content; the browser's default places it after the inserted content. When you
preventDefault, you take on the responsibility to position the cursor correctly. - "Rich content pastes as plain text." The
text/plainbranch was the only one that worked. Add explicit handling fortext/htmlandfilesbefore falling through.
Observability
When a user reports "paste is broken", ask them what they pasted from and into. Paste the same content into Clipboard Inspector to see exactly what the browser delivers — whether a text/plain value was actually present, whether the HTML contained the markers your handler looked for. The difference between what the inspector shows and what your handler did with it points directly at the bug. For a deeper reference on the event shape, see debugging paste events.