Overview
The Clipboard API provides programmatic access to the system clipboard. In contenteditable elements, you can use it alongside paste/copy/cut events to control how content is copied and pasted.
⚠️ Security Restrictions
Clipboard API requires secure context:
- Must be served over HTTPS (or localhost)
- User interaction required (cannot access clipboard in background)
- Browser may prompt user for permission
For older browsers or when Clipboard API is unavailable, use the paste, copy, and cut events with clipboardData.
Writing Text to Clipboard
Use navigator.clipboard.writeText() to copy plain text to the clipboard.
// Copy text to clipboard
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Text copied to clipboard');
} catch (err) {
console.error('Failed to copy:', err);
}
}Reading Text from Clipboard
Use navigator.clipboard.readText() to read plain text from the clipboard.
// Read text from clipboard
async function pasteText() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted text:', text);
return text;
} catch (err) {
console.error('Failed to read clipboard:', err);
}
}Permission required: Reading from clipboard requires user permission. The browser will prompt the user if permission hasn't been granted.
Writing HTML to Clipboard
To copy HTML content, use ClipboardItem with a Blob containing the HTML.
// Copy HTML to clipboard
async function copyHTML(html) {
try {
const blob = new Blob([html], { type: 'text/html' });
const clipboardItem = new ClipboardItem({ 'text/html': blob });
await navigator.clipboard.write([clipboardItem]);
} catch (err) {
console.error('Failed to copy HTML:', err);
}
}Reading HTML from Clipboard
Read HTML from clipboard by checking for text/html type in clipboard items.
// Read HTML from clipboard
async function pasteHTML() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const item of clipboardItems) {
if (item.types.includes('text/html')) {
const blob = await item.getType('text/html');
const html = await blob.text();
return html;
}
}
} catch (err) {
console.error('Failed to read HTML:', err);
}
}Handling Paste Events
The paste event fires when the user pastes content. You can intercept it and customize how content is inserted.
// Handle paste event
element.addEventListener('paste', async (e) => {
e.preventDefault(); // Prevent default paste behavior
const clipboardData = e.clipboardData || window.clipboardData;
// Get plain text
const text = clipboardData.getData('text/plain');
// Get HTML (if available)
const html = clipboardData.getData('text/html');
// Insert at selection
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
if (html) {
// Parse and insert HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const fragment = document.createDocumentFragment();
while (tempDiv.firstChild) {
fragment.appendChild(tempDiv.firstChild);
}
range.insertNode(fragment);
} else {
// Insert plain text
const textNode = document.createTextNode(text);
range.insertNode(textNode);
}
}
});⚠️ Security: Sanitize HTML
Always sanitize pasted HTML: Pasted content may contain malicious scripts or event handlers. Always sanitize HTML before inserting it into the DOM.
// Sanitize HTML before pasting
function sanitizeHTML(html) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Remove script tags
const scripts = tempDiv.querySelectorAll('script');
scripts.forEach(script => script.remove());
// Remove event handlers (onclick, etc.)
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach(el => {
Array.from(el.attributes).forEach(attr => {
if (attr.name.startsWith('on')) {
el.removeAttribute(attr.name);
}
});
});
return tempDiv.innerHTML;
}Handling Copy Events
The copy event fires when the user copies content. You can customize what gets copied.
// Handle copy event
element.addEventListener('copy', (e) => {
e.preventDefault(); // Prevent default copy behavior
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const text = range.toString();
const html = range.cloneContents();
// Set clipboard data
const clipboardData = e.clipboardData || window.clipboardData;
clipboardData.setData('text/plain', text);
// Create HTML string from fragment
const tempDiv = document.createElement('div');
tempDiv.appendChild(html.cloneNode(true));
clipboardData.setData('text/html', tempDiv.innerHTML);
}
});Handling Cut Events
The cut event fires when the user cuts content. Similar to copy, but also removes the content.
// Handle cut event
element.addEventListener('cut', (e) => {
e.preventDefault(); // Prevent default cut behavior
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const text = range.toString();
const html = range.cloneContents();
// Set clipboard data
const clipboardData = e.clipboardData || window.clipboardData;
clipboardData.setData('text/plain', text);
// Create HTML string from fragment
const tempDiv = document.createElement('div');
tempDiv.appendChild(html.cloneNode(true));
clipboardData.setData('text/html', tempDiv.innerHTML);
// Delete selected content
range.deleteContents();
}
});Checking Permissions
Use the Permissions API to check clipboard access permissions before attempting to read from clipboard.
// Check clipboard permissions
async function checkClipboardPermission() {
try {
const result = await navigator.permissions.query({ name: 'clipboard-read' });
console.log('Clipboard read permission:', result.state);
if (result.state === 'granted') {
// Can read clipboard
} else if (result.state === 'prompt') {
// Will prompt user
} else {
// Permission denied
}
} catch (err) {
// Permission API not supported
console.error('Permission check failed:', err);
}
}Permission states:
granted- Permission granted, can access clipboardprompt- Browser will prompt user when accessing clipboarddenied- Permission denied, cannot access clipboard
Platform-Specific Issues & Edge Cases
Clipboard API behavior and paste events can vary significantly depending on browser, OS, device, and keyboard type. These variations can affect how content is copied and pasted.
Browser-Specific Issues
⚠️ Safari: Clipboard API Limitations
Safari: Clipboard API has limitations and differences:
- Clipboard API requires macOS 10.12+ or iOS 10+
- Reading HTML from clipboard may not work in all Safari versions
- Permission prompts may appear more frequently
- Paste events may fire in different order than Chrome/Edge
⚠️ Chrome/Edge: HTML Format Variations
Chrome/Edge: Pasted HTML format may vary:
- HTML from different sources (Word, Google Docs, etc.) may have different structures
- Inline styles vs semantic HTML may differ
- Event timing:
pastemay fire beforeinputevent
⚠️ Firefox: Clipboard Permissions
Firefox: Clipboard API permissions may behave differently:
- Permission prompts may be more restrictive
- Reading clipboard may require explicit user gesture in some versions
- HTML clipboard format may differ from Chrome
OS & Keyboard-Specific Issues
⚠️ macOS: Permission Prompts
macOS: Clipboard access may trigger system-level permission prompts:
- First clipboard read may require user approval in system preferences
- Korean IME composition may interfere with clipboard operations
- Paste operations during composition may be delayed or fail
⚠️ Windows: IME and Clipboard
Windows: IME composition may affect clipboard operations:
- Pasting during active IME composition may cancel composition
- Clipboard content may include composition text unexpectedly
- Different IME providers may handle clipboard differently
⚠️ Linux: Clipboard Integration
Linux: Clipboard behavior varies by desktop environment:
- X11 vs Wayland may have different clipboard behavior
- Primary selection (middle-click paste) may interfere
- IME integration may vary by distribution
Device-Specific Issues
⚠️ Mobile: Clipboard API Restrictions
Mobile devices (Android/iOS): Clipboard API has additional restrictions:
- iOS: Clipboard access requires user interaction and may show permission alerts
- Android: Clipboard access may be restricted by browser or OS security policies
- Virtual keyboard may interfere with paste operations
- Paste events may fire differently than desktop
- HTML clipboard format may be simplified or missing on mobile
⚠️ Mobile Keyboards: Text Prediction Interference
Mobile keyboards with text prediction:
- Samsung Keyboard: Paste operations may trigger text prediction suggestions
- Gboard: Clipboard suggestions may interfere with paste events
- iOS QuickType: May modify pasted content
⚠️ Samsung Keyboard: Clipboard History & Saved Text
Android + Samsung Keyboard: Samsung Keyboard provides clipboard history and saved text insertion features that may behave differently:
- Clipboard History: Selecting from clipboard history may fire
insertFromPasteorinsertReplacementTextdepending on context - Saved Text: Inserting saved text may not fire
beforeinputevent, making it impossible to intercept - Multiple Events: A single clipboard history selection may trigger multiple
inputevents - Event Timing: Events may fire in unexpected order or with delays
- Content Replacement: Saved text may replace more content than expected
Impact: Clipboard history and saved text features can interfere with custom editing logic, undo/redo stacks, and change tracking. Always monitor both beforeinput and input events, and compare DOM state to detect unexpected insertions.
Handling Samsung Keyboard Clipboard Features
- Monitor both events: Listen to both
beforeinputandinputto catch all clipboard insertions - Compare DOM state: Store DOM state before events and compare after to detect bulk insertions from clipboard history
- Handle missing beforeinput: If
beforeinputdoesn't fire, handle ininputevent - Multiple input events: Be prepared for multiple
inputevents for a single clipboard selection - Sanitize content: Always sanitize content from clipboard history or saved text, as it may contain unexpected HTML or formatting
⚠️ Tablet: Hybrid Clipboard Behavior
Tablets: Clipboard behavior may vary based on input method:
- External keyboard: behaves like desktop
- Touch input: behaves like mobile
- Stylus input: may have different paste behavior
General Browser Differences
Clipboard API support: The modern Clipboard API is supported in Chrome, Edge, Firefox, and Safari (Safari 13.1+). Older browsers require the event-based approach with clipboardData.
Event timing: The paste event may fire before or after the input event with inputType: 'insertFromPaste', depending on the browser.
HTML format: Different applications may paste HTML in different formats. Some include inline styles, others use semantic HTML. Always normalize pasted HTML to match your editor's format.