Clipboard API in Practice: Browser Clipboard Read/Write and Security Policies

技术架构(Updated Jun 3, 2026)

Clipboard API: The Modern Clipboard Interaction Standard

Browser clipboard operations have evolved from document.execCommand to the Async Clipboard API:

Aspect execCommand('copy') Async Clipboard API
Standard Deprecated W3C recommended
Async No (sync blocking) Yes (Promise-based)
Permissions Implicit (user gesture) Explicit Permissions API
Rich text Not supported Supported (ClipboardItem)
Custom formats Not supported Supported (web custom formats)
Image read/write Not supported Supported
Reliability Depends on selection Direct clipboard access

Core API Deep Dive

Clipboard API Interfaces

interface Clipboard extends EventTarget {
  read(): Promise<ClipboardItem[]>;
  readText(): Promise<string>;
  write(data: ClipboardItem[]): Promise<void>;
  writeText(text: string): Promise<void>;
}

interface ClipboardItem {
  readonly types: string[];
  getType(type: string): Promise<Blob>;
}

Text Read/Write

async function copyText(text: string) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('Copied to clipboard');
  } catch (err) {
    console.error('Copy failed:', err);
    fallbackCopyText(text);
  }
}

async function pasteText(): Promise<string> {
  try {
    const text = await navigator.clipboard.readText();
    return text;
  } catch (err) {
    console.error('Read failed:', err);
    return '';
  }
}

Rich Text and Multi-Format Writing

ClipboardItem Multi-Format

async function copyRichText(html: string, plainText: string) {
  const clipboardItem = new ClipboardItem({
    'text/html': new Blob([html], { type: 'text/html' }),
    'text/plain': new Blob([plainText], { type: 'text/plain' }),
  });
  await navigator.clipboard.write([clipboardItem]);
}

copyRichText(
  '<strong>Important</strong>: Submit by <em>Friday</em>',
  'Important: Submit by Friday'
);

Image Copy

async function copyImage(pngBlob: Blob) {
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': pngBlob })
  ]);
}

async function copyCanvas(canvas: HTMLCanvasElement) {
  const blob = await new Promise<Blob>((resolve) => {
    canvas.toBlob((b) => resolve(b!), 'image/png');
  });
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': blob })
  ]);
}

Image Paste

document.addEventListener('paste', async (e) => {
  const items = e.clipboardData?.items;
  if (!items) return;

  for (const item of items) {
    if (item.type.startsWith('image/')) {
      const blob = item.getAsFile()!;
      const url = URL.createObjectURL(blob);
      const img = document.createElement('img');
      img.src = url;
      document.body.appendChild(img);
    }
  }
});

Custom MIME Types (Web Custom Formats)

async function copyCustomData(data: object) {
  const json = JSON.stringify(data);
  await navigator.clipboard.write([
    new ClipboardItem({
      'web application/json': new Blob([json], { type: 'application/json' }),
      'text/plain': new Blob([json], { type: 'text/plain' }),
    })
  ]);
}

async function pasteCustomData(): Promise<object | null> {
  const items = await navigator.clipboard.read();
  for (const item of items) {
    if (item.types.includes('web application/json')) {
      const blob = await item.getType('web application/json');
      const text = await blob.text();
      return JSON.parse(text);
    }
  }
  return null;
}

The web prefix is a security requirement for custom formats, preventing pages from reading native clipboard formats.


Permissions and Security Policies

Permissions API

async function checkClipboardPermission(): Promise<boolean> {
  try {
    const result = await navigator.permissions.query({
      name: 'clipboard-read' as PermissionName
    });
    return result.state === 'granted';
  } catch {
    return false;
  }
}

Security Restrictions

Operation Requirement Notes
writeText User activation Needs click/keydown gesture
write User activation Same as above
readText User activation + permission User authorization prompt
read User activation + permission User authorization prompt
Focused window Document must be focused Unfocused window calls rejected

User Activation Check

button.addEventListener('click', async () => {
  // ✅ Inside click event — user activation present
  await navigator.clipboard.writeText('copied!');
});

setTimeout(async () => {
  await navigator.clipboard.writeText('fails'); // ❌ DOMException
}, 1000);

Fallback: execCommand

function fallbackCopyText(text: string): boolean {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.style.position = 'fixed';
  textarea.style.left = '-9999px';
  document.body.appendChild(textarea);
  textarea.select();

  try {
    const success = document.execCommand('copy');
    document.body.removeChild(textarea);
    return success;
  } catch {
    document.body.removeChild(textarea);
    return false;
  }
}

async function safeCopy(text: string) {
  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch {}
  }
  return fallbackCopyText(text);
}

Deep Paste Event Handling

Intercept and Transform Paste Content

editor.addEventListener('paste', (e) => {
  e.preventDefault();
  const clipboardData = e.clipboardData!;

  const html = clipboardData.getData('text/html');
  if (html) {
    const sanitized = sanitizeHTML(html);
    insertHTML(sanitized);
    return;
  }

  const text = clipboardData.getData('text/plain');
  if (text) {
    const linked = text.replace(
      /https?:\/\/\S+/g,
      (url) => `<a href="${url}">${url}</a>`
    );
    insertHTML(linked);
  }
});

Clipboard Data Type Detection

editor.addEventListener('paste', (e) => {
  const data = e.clipboardData!;
  console.log('Types:', [...data.types]);

  const priorityOrder = [
    'text/html',
    'application/json',
    'text/plain',
    'image/png',
  ];

  for (const type of priorityOrder) {
    if (data.types.includes(type)) {
      const content = data.getData(type);
      console.log(`Paste ${type}:`, content.slice(0, 100));
      break;
    }
  }
});

Practice: Copy Button Component

class CopyButton {
  private timeoutId: number | null = null;

  constructor(private button: HTMLButtonElement, private getText: () => string) {
    button.addEventListener('click', () => this.handleCopy());
  }

  private async handleCopy() {
    const text = this.getText();
    const success = await safeCopy(text);

    if (success) {
      this.showFeedback('Copied!');
    } else {
      this.showFeedback('Failed');
    }
  }

  private showFeedback(message: string) {
    const original = this.button.textContent;
    this.button.textContent = message;
    this.button.disabled = true;

    if (this.timeoutId) clearTimeout(this.timeoutId);
    this.timeoutId = window.setTimeout(() => {
      this.button.textContent = original;
      this.button.disabled = false;
    }, 2000);
  }
}

Browser Compatibility

API Chrome Firefox Safari Edge
writeText 66+ 63+ 13.1+ 79+
readText 66+ 63+ 13.1+ 79+
write (ClipboardItem) 76+ 87+ 13.1+ 79+
read 76+ 87+ 13.1+ 79+
Web Custom Formats 104+ No No 104+
Image clipboard 76+ 87+ 13.1+ 79+

Best Practices

  1. Always provide fallback: Use execCommand when Clipboard API is unavailable
  2. Call within user activation: Copy must be triggered in click/keydown handlers
  3. Always show feedback: Visual feedback for both success and failure
  4. Read with caution: read() involves privacy—request only when necessary
  5. Sanitize paste content: HTML paste must be filtered for XSS risks

Summary

The Async Clipboard API is the standard approach for modern browser clipboard operations, supporting text, rich text, image, and custom format read/write. More reliable, secure, and capable than execCommand, but requires attention to user activation and permission models.

Use Clipboard Viewer to debug clipboard content, Text Replace for batch paste processing, and Case Convert to normalize clipboard text.

Try these browser-local tools — no sign-up required →

#Clipboard API#剪贴板#异步API#浏览器安全#用户交互