JavaScript APIs

JavaScript Clipboard API: Secure Copy and Paste Operations

Master the Clipboard API for secure programmatic access to clipboard operations. Learn to implement copy/paste functionality, handle different data types, and follow best practices.

By JavaScript Document Team
clipboard-apiweb-apisuser-interactionsecurityasync

The Clipboard API provides a secure way to read from and write to the system clipboard. It replaces the older document.execCommand() approach with a modern, promise-based API that handles permissions and supports various data types.

Understanding the Clipboard API

The Clipboard API offers asynchronous methods to interact with the clipboard, ensuring better security and user control over clipboard access.

Basic Clipboard Operations

// Modern Clipboard API
async function copyText() {
  try {
    await navigator.clipboard.writeText('Hello, Clipboard!');
    console.log('Text copied to clipboard');
  } catch (err) {
    console.error('Failed to copy text: ', err);
  }
}

// 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);
  }
}

// Copy selected text
async function copySelection() {
  const selection = window.getSelection().toString();
  if (selection) {
    try {
      await navigator.clipboard.writeText(selection);
      console.log('Selection copied');
    } catch (err) {
      console.error('Failed to copy selection: ', err);
    }
  }
}

// Copy element content
async function copyElementContent(elementId) {
  const element = document.getElementById(elementId);
  if (element) {
    try {
      await navigator.clipboard.writeText(element.textContent);
      console.log('Element content copied');
    } catch (err) {
      console.error('Failed to copy element content: ', err);
    }
  }
}

Working with ClipboardItem

// Copy multiple types of data
async function copyRichContent(text, htmlContent) {
  try {
    const clipboardItem = new ClipboardItem({
      'text/plain': new Blob([text], { type: 'text/plain' }),
      'text/html': new Blob([htmlContent], { type: 'text/html' }),
    });

    await navigator.clipboard.write([clipboardItem]);
    console.log('Rich content copied');
  } catch (err) {
    console.error('Failed to copy rich content: ', err);
  }
}

// Read clipboard items
async function readClipboardItems() {
  try {
    const items = await navigator.clipboard.read();

    for (const item of items) {
      console.log('Available types:', item.types);

      for (const type of item.types) {
        const blob = await item.getType(type);

        if (type === 'text/plain') {
          const text = await blob.text();
          console.log('Text content:', text);
        } else if (type === 'text/html') {
          const html = await blob.text();
          console.log('HTML content:', html);
        } else if (type.startsWith('image/')) {
          const image = URL.createObjectURL(blob);
          console.log('Image URL:', image);
        }
      }
    }
  } catch (err) {
    console.error('Failed to read clipboard items: ', err);
  }
}

// Copy image to clipboard
async function copyImage(imageUrl) {
  try {
    const response = await fetch(imageUrl);
    const blob = await response.blob();

    const clipboardItem = new ClipboardItem({
      [blob.type]: blob,
    });

    await navigator.clipboard.write([clipboardItem]);
    console.log('Image copied to clipboard');
  } catch (err) {
    console.error('Failed to copy image: ', err);
  }
}

// Copy canvas content
async function copyCanvas(canvas) {
  try {
    const blob = await new Promise((resolve) =>
      canvas.toBlob(resolve, 'image/png')
    );

    const clipboardItem = new ClipboardItem({
      'image/png': blob,
    });

    await navigator.clipboard.write([clipboardItem]);
    console.log('Canvas copied to clipboard');
  } catch (err) {
    console.error('Failed to copy canvas: ', err);
  }
}

Practical Applications

Advanced Copy Button

class CopyButton {
  constructor(buttonElement, targetElement) {
    this.button = buttonElement;
    this.target = targetElement;
    this.originalText = this.button.textContent;

    this.init();
  }

  init() {
    this.button.addEventListener('click', () => this.copy());
  }

  async copy() {
    const content = this.getContent();

    try {
      await navigator.clipboard.writeText(content);
      this.showSuccess();
    } catch (err) {
      this.showError();
      // Fallback to older method
      this.fallbackCopy(content);
    }
  }

  getContent() {
    if (typeof this.target === 'string') {
      return this.target;
    } else if (
      this.target instanceof HTMLInputElement ||
      this.target instanceof HTMLTextAreaElement
    ) {
      return this.target.value;
    } else {
      return this.target.textContent;
    }
  }

  showSuccess() {
    this.button.textContent = '✓ Copied!';
    this.button.classList.add('copy-success');

    setTimeout(() => {
      this.button.textContent = this.originalText;
      this.button.classList.remove('copy-success');
    }, 2000);
  }

  showError() {
    this.button.textContent = '✗ Failed';
    this.button.classList.add('copy-error');

    setTimeout(() => {
      this.button.textContent = this.originalText;
      this.button.classList.remove('copy-error');
    }, 2000);
  }

  fallbackCopy(text) {
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';

    document.body.appendChild(textarea);
    textarea.select();

    try {
      document.execCommand('copy');
      this.showSuccess();
    } catch (err) {
      console.error('Fallback copy failed:', err);
    }

    document.body.removeChild(textarea);
  }
}

// Code snippet copy functionality
class CodeCopy {
  constructor(container) {
    this.container = container;
    this.init();
  }

  init() {
    const codeBlocks = this.container.querySelectorAll('pre code');

    codeBlocks.forEach((block) => {
      const wrapper = document.createElement('div');
      wrapper.className = 'code-block-wrapper';

      block.parentNode.insertBefore(wrapper, block);
      wrapper.appendChild(block);

      const copyButton = document.createElement('button');
      copyButton.className = 'code-copy-button';
      copyButton.textContent = 'Copy';
      wrapper.appendChild(copyButton);

      new CopyButton(copyButton, block);
    });
  }
}

// Table data copy
class TableCopy {
  constructor(table) {
    this.table = table;
    this.init();
  }

  init() {
    this.addCopyButtons();
    this.setupKeyboardShortcuts();
  }

  addCopyButtons() {
    // Add copy row buttons
    const rows = this.table.querySelectorAll('tbody tr');
    rows.forEach((row) => {
      const copyCell = document.createElement('td');
      const copyButton = document.createElement('button');
      copyButton.textContent = '📋';
      copyButton.title = 'Copy row';
      copyButton.addEventListener('click', () => this.copyRow(row));
      copyCell.appendChild(copyButton);
      row.appendChild(copyCell);
    });

    // Add copy column buttons
    const headers = this.table.querySelectorAll('thead th');
    headers.forEach((header, index) => {
      const copyButton = document.createElement('button');
      copyButton.textContent = '📋';
      copyButton.title = 'Copy column';
      copyButton.addEventListener('click', () => this.copyColumn(index));
      header.appendChild(copyButton);
    });
  }

  async copyRow(row) {
    const cells = Array.from(row.querySelectorAll('td'));
    const text = cells.map((cell) => cell.textContent.trim()).join('\t');

    try {
      await navigator.clipboard.writeText(text);
      this.showCopyFeedback(row);
    } catch (err) {
      console.error('Failed to copy row:', err);
    }
  }

  async copyColumn(columnIndex) {
    const cells = this.table.querySelectorAll(
      `tbody tr td:nth-child(${columnIndex + 1})`
    );
    const values = Array.from(cells).map((cell) => cell.textContent.trim());
    const text = values.join('\n');

    try {
      await navigator.clipboard.writeText(text);
      this.showColumnFeedback(columnIndex);
    } catch (err) {
      console.error('Failed to copy column:', err);
    }
  }

  async copyTable() {
    const rows = Array.from(this.table.querySelectorAll('tr'));
    const data = rows.map((row) => {
      const cells = Array.from(row.querySelectorAll('td, th'));
      return cells.map((cell) => cell.textContent.trim()).join('\t');
    });

    const text = data.join('\n');

    try {
      await navigator.clipboard.writeText(text);
      this.showTableFeedback();
    } catch (err) {
      console.error('Failed to copy table:', err);
    }
  }

  setupKeyboardShortcuts() {
    this.table.addEventListener('keydown', async (e) => {
      if (e.ctrlKey || e.metaKey) {
        if (e.key === 'c') {
          e.preventDefault();
          await this.copySelection();
        }
      }
    });
  }

  async copySelection() {
    const selection = window.getSelection();
    const selectedCells = this.getSelectedCells(selection);

    if (selectedCells.length > 0) {
      const text = this.formatCellsAsText(selectedCells);
      await navigator.clipboard.writeText(text);
    }
  }

  getSelectedCells(selection) {
    const cells = [];
    for (let i = 0; i < selection.rangeCount; i++) {
      const range = selection.getRangeAt(i);
      const cellsInRange = this.table.querySelectorAll('td, th');

      cellsInRange.forEach((cell) => {
        if (range.intersectsNode(cell)) {
          cells.push(cell);
        }
      });
    }
    return cells;
  }

  formatCellsAsText(cells) {
    // Group cells by row
    const rows = new Map();

    cells.forEach((cell) => {
      const row = cell.parentElement;
      if (!rows.has(row)) {
        rows.set(row, []);
      }
      rows.get(row).push(cell);
    });

    // Format as tab-separated values
    const text = Array.from(rows.values())
      .map((rowCells) =>
        rowCells.map((cell) => cell.textContent.trim()).join('\t')
      )
      .join('\n');

    return text;
  }

  showCopyFeedback(element) {
    element.classList.add('copied');
    setTimeout(() => element.classList.remove('copied'), 1000);
  }

  showColumnFeedback(columnIndex) {
    const cells = this.table.querySelectorAll(
      `td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`
    );
    cells.forEach((cell) => this.showCopyFeedback(cell));
  }

  showTableFeedback() {
    this.table.classList.add('copied');
    setTimeout(() => this.table.classList.remove('copied'), 1000);
  }
}

Rich Content Copy

// Rich content editor with copy/paste
class RichContentEditor {
  constructor(editorElement) {
    this.editor = editorElement;
    this.init();
  }

  init() {
    this.editor.addEventListener('paste', (e) => this.handlePaste(e));
    this.editor.addEventListener('copy', (e) => this.handleCopy(e));
    this.editor.addEventListener('cut', (e) => this.handleCut(e));
  }

  async handlePaste(e) {
    e.preventDefault();

    // Try to read clipboard items
    try {
      const items = await navigator.clipboard.read();

      for (const item of items) {
        // Handle images
        if (
          item.types.includes('image/png') ||
          item.types.includes('image/jpeg')
        ) {
          const blob = await item.getType(
            item.types.find((type) => type.startsWith('image/'))
          );
          this.insertImage(blob);
        }
        // Handle HTML
        else if (item.types.includes('text/html')) {
          const blob = await item.getType('text/html');
          const html = await blob.text();
          this.insertHTML(this.sanitizeHTML(html));
        }
        // Handle plain text
        else if (item.types.includes('text/plain')) {
          const blob = await item.getType('text/plain');
          const text = await blob.text();
          this.insertText(text);
        }
      }
    } catch (err) {
      // Fallback to clipboard data
      const clipboardData = e.clipboardData;

      if (clipboardData.types.includes('text/html')) {
        const html = clipboardData.getData('text/html');
        this.insertHTML(this.sanitizeHTML(html));
      } else {
        const text = clipboardData.getData('text/plain');
        this.insertText(text);
      }
    }
  }

  async handleCopy(e) {
    const selection = window.getSelection();
    if (!selection.toString()) return;

    e.preventDefault();

    const range = selection.getRangeAt(0);
    const container = document.createElement('div');
    container.appendChild(range.cloneContents());

    const html = container.innerHTML;
    const text = container.textContent;

    try {
      const clipboardItem = new ClipboardItem({
        'text/plain': new Blob([text], { type: 'text/plain' }),
        'text/html': new Blob([html], { type: 'text/html' }),
      });

      await navigator.clipboard.write([clipboardItem]);
    } catch (err) {
      // Fallback
      e.clipboardData.setData('text/plain', text);
      e.clipboardData.setData('text/html', html);
    }
  }

  async handleCut(e) {
    await this.handleCopy(e);

    const selection = window.getSelection();
    if (selection.toString()) {
      selection.deleteFromDocument();
    }
  }

  insertImage(blob) {
    const url = URL.createObjectURL(blob);
    const img = document.createElement('img');
    img.src = url;
    img.style.maxWidth = '100%';

    this.insertAtCursor(img);

    // Clean up object URL after image loads
    img.onload = () => URL.revokeObjectURL(url);
  }

  insertHTML(html) {
    const template = document.createElement('template');
    template.innerHTML = html;

    const fragment = template.content;
    this.insertAtCursor(fragment);
  }

  insertText(text) {
    const textNode = document.createTextNode(text);
    this.insertAtCursor(textNode);
  }

  insertAtCursor(content) {
    const selection = window.getSelection();

    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      range.deleteContents();

      if (content instanceof DocumentFragment) {
        range.insertNode(content);
      } else {
        range.insertNode(content);
      }

      range.collapse(false);
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }

  sanitizeHTML(html) {
    // Basic HTML sanitization - use DOMPurify in production
    const template = document.createElement('template');
    template.innerHTML = html;

    // Remove script tags and event handlers
    const scripts = template.content.querySelectorAll('script');
    scripts.forEach((script) => script.remove());

    const elements = template.content.querySelectorAll('*');
    elements.forEach((el) => {
      // Remove event handlers
      Array.from(el.attributes).forEach((attr) => {
        if (attr.name.startsWith('on')) {
          el.removeAttribute(attr.name);
        }
      });
    });

    return template.innerHTML;
  }
}

// Formatted data copy
class FormattedDataCopy {
  static async copyAsJSON(data) {
    const json = JSON.stringify(data, null, 2);

    try {
      await navigator.clipboard.writeText(json);
      return true;
    } catch (err) {
      console.error('Failed to copy JSON:', err);
      return false;
    }
  }

  static async copyAsCSV(data, headers) {
    const csv = this.convertToCSV(data, headers);

    try {
      await navigator.clipboard.writeText(csv);
      return true;
    } catch (err) {
      console.error('Failed to copy CSV:', err);
      return false;
    }
  }

  static async copyAsMarkdown(data, headers) {
    const markdown = this.convertToMarkdown(data, headers);

    try {
      await navigator.clipboard.writeText(markdown);
      return true;
    } catch (err) {
      console.error('Failed to copy Markdown:', err);
      return false;
    }
  }

  static convertToCSV(data, headers) {
    const rows = [];

    if (headers) {
      rows.push(headers.join(','));
    }

    data.forEach((row) => {
      const values = Object.values(row).map((value) => {
        // Escape values containing commas or quotes
        if (
          typeof value === 'string' &&
          (value.includes(',') || value.includes('"'))
        ) {
          return `"${value.replace(/"/g, '""')}"`;
        }
        return value;
      });

      rows.push(values.join(','));
    });

    return rows.join('\n');
  }

  static convertToMarkdown(data, headers) {
    const rows = [];

    if (headers) {
      rows.push('| ' + headers.join(' | ') + ' |');
      rows.push('| ' + headers.map(() => '---').join(' | ') + ' |');
    }

    data.forEach((row) => {
      const values = Object.values(row).map((value) => String(value));
      rows.push('| ' + values.join(' | ') + ' |');
    });

    return rows.join('\n');
  }
}

Clipboard Permissions

// Permission handling
class ClipboardPermissions {
  static async checkReadPermission() {
    try {
      const result = await navigator.permissions.query({
        name: 'clipboard-read',
      });
      return result.state;
    } catch (err) {
      console.error('Failed to check clipboard read permission:', err);
      return 'denied';
    }
  }

  static async checkWritePermission() {
    try {
      const result = await navigator.permissions.query({
        name: 'clipboard-write',
      });
      return result.state;
    } catch (err) {
      // Write permission is usually granted by default
      return 'granted';
    }
  }

  static async requestReadPermission() {
    try {
      // Reading will prompt for permission if needed
      await navigator.clipboard.readText();
      return true;
    } catch (err) {
      console.error('Failed to get clipboard read permission:', err);
      return false;
    }
  }

  static monitorPermissions(callback) {
    navigator.permissions.query({ name: 'clipboard-read' }).then((result) => {
      callback('read', result.state);

      result.addEventListener('change', () => {
        callback('read', result.state);
      });
    });

    navigator.permissions
      .query({ name: 'clipboard-write' })
      .then((result) => {
        callback('write', result.state);

        result.addEventListener('change', () => {
          callback('write', result.state);
        });
      })
      .catch(() => {
        // Write permission query not supported
        callback('write', 'granted');
      });
  }
}

// Clipboard monitor
class ClipboardMonitor {
  constructor() {
    this.listeners = new Set();
    this.lastContent = null;
    this.monitoring = false;
  }

  start() {
    if (this.monitoring) return;

    this.monitoring = true;
    this.checkClipboard();
  }

  stop() {
    this.monitoring = false;
  }

  async checkClipboard() {
    if (!this.monitoring) return;

    try {
      const permission = await ClipboardPermissions.checkReadPermission();

      if (permission === 'granted') {
        const content = await navigator.clipboard.readText();

        if (content !== this.lastContent) {
          this.lastContent = content;
          this.notifyListeners(content);
        }
      }
    } catch (err) {
      // Silent fail - user may have denied permission
    }

    // Check again after delay
    setTimeout(() => this.checkClipboard(), 1000);
  }

  addListener(callback) {
    this.listeners.add(callback);
  }

  removeListener(callback) {
    this.listeners.delete(callback);
  }

  notifyListeners(content) {
    this.listeners.forEach((callback) => {
      try {
        callback(content);
      } catch (err) {
        console.error('Clipboard listener error:', err);
      }
    });
  }
}

Share and Copy Integration

// Share with copy fallback
class ShareWithCopy {
  constructor(data) {
    this.data = data;
  }

  async share() {
    // Try Web Share API first
    if (navigator.share) {
      try {
        await navigator.share(this.data);
        return { method: 'share', success: true };
      } catch (err) {
        if (err.name === 'AbortError') {
          return { method: 'share', success: false, reason: 'cancelled' };
        }
      }
    }

    // Fallback to clipboard
    return await this.copyToClipboard();
  }

  async copyToClipboard() {
    const text = this.formatForClipboard();

    try {
      await navigator.clipboard.writeText(text);
      return { method: 'clipboard', success: true };
    } catch (err) {
      return { method: 'clipboard', success: false, error: err };
    }
  }

  formatForClipboard() {
    const parts = [];

    if (this.data.title) {
      parts.push(this.data.title);
    }

    if (this.data.text) {
      parts.push(this.data.text);
    }

    if (this.data.url) {
      parts.push(this.data.url);
    }

    return parts.join('\n\n');
  }

  static createShareButton(buttonElement, data) {
    const sharer = new ShareWithCopy(data);

    buttonElement.addEventListener('click', async () => {
      const result = await sharer.share();

      if (result.success) {
        if (result.method === 'clipboard') {
          // Show copy feedback
          buttonElement.textContent = 'Copied!';
          setTimeout(() => {
            buttonElement.textContent = 'Share';
          }, 2000);
        }
      } else {
        console.error('Share failed:', result);
      }
    });
  }
}

// URL shortener with copy
class URLShortenerWithCopy {
  constructor(apiEndpoint) {
    this.apiEndpoint = apiEndpoint;
  }

  async shortenAndCopy(longUrl) {
    try {
      // Call your URL shortening API
      const response = await fetch(this.apiEndpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url: longUrl }),
      });

      const data = await response.json();
      const shortUrl = data.shortUrl;

      // Copy to clipboard
      await navigator.clipboard.writeText(shortUrl);

      return { success: true, shortUrl };
    } catch (err) {
      console.error('Failed to shorten and copy URL:', err);

      // Fallback: just copy the long URL
      try {
        await navigator.clipboard.writeText(longUrl);
        return { success: true, shortUrl: longUrl, fallback: true };
      } catch (copyErr) {
        return { success: false, error: copyErr };
      }
    }
  }
}

Best Practices

  1. Always handle errors gracefully
async function safeCopy(text) {
  try {
    await navigator.clipboard.writeText(text);
    return { success: true };
  } catch (err) {
    // Fallback for older browsers
    if (document.execCommand) {
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.style.position = 'fixed';
      textarea.style.opacity = '0';
      document.body.appendChild(textarea);
      textarea.select();

      try {
        document.execCommand('copy');
        document.body.removeChild(textarea);
        return { success: true, fallback: true };
      } catch (fallbackErr) {
        document.body.removeChild(textarea);
        return { success: false, error: fallbackErr };
      }
    }

    return { success: false, error: err };
  }
}
  1. Provide user feedback
class CopyFeedback {
  static show(element, success = true) {
    const feedback = document.createElement('div');
    feedback.className = `copy-feedback ${success ? 'success' : 'error'}`;
    feedback.textContent = success ? 'Copied!' : 'Failed to copy';

    const rect = element.getBoundingClientRect();
    feedback.style.position = 'fixed';
    feedback.style.top = `${rect.top - 40}px`;
    feedback.style.left = `${rect.left + rect.width / 2}px`;
    feedback.style.transform = 'translateX(-50%)';

    document.body.appendChild(feedback);

    setTimeout(() => {
      feedback.classList.add('fade-out');
      setTimeout(() => feedback.remove(), 300);
    }, 2000);
  }
}
  1. Check permissions before attempting to read
async function safeRead() {
  try {
    const permission = await navigator.permissions.query({
      name: 'clipboard-read',
    });

    if (permission.state === 'denied') {
      throw new Error('Clipboard access denied');
    }

    return await navigator.clipboard.readText();
  } catch (err) {
    console.error('Cannot read clipboard:', err);
    return null;
  }
}
  1. Use appropriate MIME types
const mimeTypes = {
  text: 'text/plain',
  html: 'text/html',
  png: 'image/png',
  jpeg: 'image/jpeg',
  svg: 'image/svg+xml',
  json: 'application/json',
};

async function copyWithType(content, type) {
  const blob = new Blob([content], { type: mimeTypes[type] || type });
  const item = new ClipboardItem({ [blob.type]: blob });
  await navigator.clipboard.write([item]);
}

Conclusion

The Clipboard API provides a secure, modern way to interact with the system clipboard. With support for various data types, async operations, and proper permission handling, it enables rich copy/paste functionality in web applications. By following best practices and providing proper fallbacks, you can create robust clipboard interactions that enhance user experience while respecting security and privacy concerns.