Clipboard API 実践:ブラウザクリップボード読み書きとセキュリティ戦略

技术架构(更新: 2026年6月3日)

Clipboard API:モダンクリップボード操作の標準

ブラウザのクリップボード操作は document.execCommand から Async Clipboard API へ進化しました:

比較項目 execCommand('copy') Async Clipboard API
標準 非推奨 W3C 推奨
非同期 いいえ(同期ブロック) はい(Promise)
権限 暗黙(ユーザー操作必要) 明示 Permissions API
リッチテキスト 非対応 対応(ClipboardItem)
カスタム形式 非対応 対応(web custom formats)
画像読み書き 非対応 対応
信頼性 選択範囲依存 クリップボード直接操作

コア API 詳解

Clipboard API インターフェース

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

テキスト読み書き

async function copyText(text: string) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('クリップボードにコピーしました');
  } catch (err) {
    console.error('コピーエラー:', err);
    fallbackCopyText(text);
  }
}

async function pasteText(): Promise<string> {
  try {
    const text = await navigator.clipboard.readText();
    return text;
  } catch (err) {
    console.error('読み取りエラー:', err);
    return '';
  }
}

リッチテキストと多形式書き込み

ClipboardItem 多形式

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>重要</strong>:<em>金曜日</em>までに提出',
  '重要:金曜日までに提出'
);

画像コピー

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

画像貼り付け

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

カスタム MIME タイプ(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;
}

web プレフィックスはカスタム形式のセキュリティ要件で、ページがネイティブクリップボード形式を読むのを防ぎます。


権限とセキュリティポリシー

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

セキュリティ制限一覧

操作 要件 説明
writeText ユーザーアクティベーション click/keydown 等のユーザージェスチャ必要
write ユーザーアクティベーション 同上
readText ユーザーアクティベーション + 権限 ユーザー認可プロンプト
read ユーザーアクティベーション + 権限 ユーザー認可プロンプト
フォーカス ドキュメントがフォーカス 非フォーカスウィンドウは拒否

ユーザーアクティベーション確認

button.addEventListener('click', async () => {
  // ✅ click イベント内 — ユーザーアクティベーションあり
  await navigator.clipboard.writeText('copied!');
});

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

フォールバック: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);
}

paste イベントの深い処理

貼り付け内容の傍受と変換

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

クリップボードデータ型検出

editor.addEventListener('paste', (e) => {
  const data = e.clipboardData!;
  console.log('タイプ一覧:', [...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(`貼り付け ${type}:`, content.slice(0, 100));
      break;
    }
  }
});

実践:コピーボタンコンポーネント

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('コピーしました!');
    } else {
      this.showFeedback('失敗');
    }
  }

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

ブラウザ対応状況

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+ 非対応 非対応 104+
画像クリップボード 76+ 87+ 13.1+ 79+

ベストプラクティス

  1. 常にフォールバック提供:Clipboard API が使えない時の execCommand 代替
  2. ユーザーアクティベーション内で呼び出し:コピーは click/keydown 内で実行
  3. フィードバック必須:成功/失敗の視覚的フィードバック
  4. 読み取りは慎重にread() はプライバシーに関わるため必要時のみ
  5. 貼り付け内容のサニタイズ:HTML 貼り付けは XSS リスクをフィルタ

まとめ

Async Clipboard API はモダンブラウザクリップボード操作の標準 API で、テキスト・リッチテキスト・画像・カスタム形式の読み書きに対応しています。execCommand より信頼性・安全性・機能性に優れますが、ユーザーアクティベーションと権限モデルに注意が必要です。

クリップボードビューア でクリップボード内容をデバッグし、テキスト置換 で貼り付けテキストを一括処理、大文字小文字変換 で正規化できます。

ブラウザローカルツールを無料で試す →

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