Clipboard API

Using the Clipboard API and paste events to handle copy, cut, and paste operations in contenteditable elements.

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 clipboard
  • prompt - Browser will prompt user when accessing clipboard
  • denied - 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: paste may fire before input event

⚠️ 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 insertFromPaste or insertReplacementText depending on context
  • Saved Text: Inserting saved text may not fire beforeinput event, making it impossible to intercept
  • Multiple Events: A single clipboard history selection may trigger multiple input events
  • 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 beforeinput and input to 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 beforeinput doesn't fire, handle in input event
  • Multiple input events: Be prepared for multiple input events 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.

Related resources