- JavaScript
JavaScriptクリップボードコピー|画像対応・動かない原因一覧
JavaScriptでクリップボードにコピーするには、await navigator.clipboard.writeText(文字列) を、ボタンのクリックなどユーザー操作の中で、HTTPS(または localhost)のページから呼びます。画像や HTML をコピーしたいときは、ClipboardItem に MIME タイプごとのデータを入れて navigator.clipboard.write() を呼びます。どちらも Promise を返すので、失敗したときに catch で理由を表示できます。
この記事では、Clipboard API の全メソッドと「動く条件」を MDN で確認した早見表、テキスト・HTML・画像(PNG)に対応したコピペ用コピーボタンを載せます。さらに、上位の解説記事にはない検証として、Playwright 1.56.1(Chromium 141.0.7390.37)で実際にボタンを押し、コピーされた内容を読み戻した結果と、HTTP・iframe・権限なしで失敗したときの実際のエラー文を掲載します。React での書き方、貼り付け(paste イベント)の読み方まで、この1本でまとまるようにしました。
Clipboard API の早見表
よく使うのは writeText(テキストを書く)と write(HTML・画像を書く)の2つです。読み取り側(readText・read)は、書き込みより条件が厳しくなっています。
| やりたいこと | 書き方 | 戻り値 | 主な条件 |
|---|---|---|---|
| テキストをコピー | await navigator.clipboard.writeText(text) |
Promise<void> |
安全なコンテキスト(HTTPS・localhost)、ユーザー操作または clipboard-write 権限 |
| HTML・画像などをコピー | await navigator.clipboard.write([new ClipboardItem({...})]) |
Promise<void> |
上と同じ。形式は text/plain・text/html・image/png が広く対応 |
| テキストを読み取り | await navigator.clipboard.readText() |
Promise<string> |
安全なコンテキスト、ユーザー操作。Chromium は clipboard-read 権限、Firefox・Safari は「貼り付け」確認メニュー |
| HTML・画像などを読み取り | await navigator.clipboard.read() |
Promise<ClipboardItem[]> |
上と同じ |
| 形式に対応しているか調べる | ClipboardItem.supports('image/png') |
boolean |
静的メソッド |
| 項目の形式一覧 | item.types |
string[] |
read() で得た ClipboardItem |
| 形式を指定して中身を取る | await item.getType('text/html') |
Promise<Blob> |
同上 |
| ユーザーのコピー・貼り付けを扱う | copy・cut・paste イベントの e.clipboardData |
DataTransfer |
ユーザーが Ctrl+C / Ctrl+V などを押したとき。HTTP でも動く |
| 以前の方法 | document.execCommand('copy') |
boolean |
MDN で非推奨(deprecated)。新しく書くコードでは Clipboard API を使う |
動く条件の早見表(MDN「Clipboard API」Security considerations より)
| 条件 | 内容 | 満たさないと |
|---|---|---|
| 安全なコンテキスト | HTTPS、または http://localhost・127.0.0.1 |
navigator.clipboard が undefined になる |
| ユーザー操作(transient user activation) | クリック・キー入力の直後に呼ぶ | 権限がなければ NotAllowedError |
| 権限(Chromium) | clipboard-write が許可済みなら操作なしでも書ける。読み取りは clipboard-read |
NotAllowedError |
| 権限(Firefox・Safari) | clipboard-read・clipboard-write 権限には対応しない(予定なし)。書き込みはユーザー操作が必要、読み取りは「貼り付け」メニューが出る |
操作外の呼び出しは失敗 |
| iframe | Chromium では親ページの <iframe allow="clipboard-read; clipboard-write"> が必要 |
NotAllowedError(permissions policy) |
| フォーカス | ページ(ドキュメント)にフォーカスがあること | NotAllowedError: Document is not focused.(Chromium の文言) |
| Web Worker | Worker からは使えない | navigator.clipboard がない |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptクリップボードコピー|画像対応・動かない原因一覧」
テキストをコピーする最小コード
まずは最小形です。await を try/catch で囲み、成功と失敗を利用者に知らせます。
<p id="order-id">ORD-2026-0001</p>
<button type="button" id="copy-btn">IDをコピー</button>
<span id="copy-status" role="status" aria-live="polite"></span>
<script>
const btn = document.querySelector('#copy-btn');
const status = document.querySelector('#copy-status');
btn.addEventListener('click', async () => {
const text = document.querySelector('#order-id').textContent.trim();
try {
await navigator.clipboard.writeText(text);
status.textContent = 'コピーしました';
} catch (err) {
status.textContent = `コピーできませんでした:${err.message}`;
}
});
</script>
ポイントは3つです。
writeTextはクリックのハンドラーの中で呼ぶ。ユーザー操作の直後であることが条件の1つだからです。awaitを付けて結果を待つ。付けないと、失敗しても「コピーしました」と表示されてしまいます。- 結果の表示は
role="status"の要素に出す。画面読み上げソフトにも「コピーしました」が伝わります。
コピペで使える完成版コピーボタン(テキスト・HTML・画像)
テキスト・リッチテキスト(HTML)・画像(PNG)の3種類を、1つのスクリプトで扱う完成版です。data-copy-text などの属性を付けたボタンを置くだけで動きます。下の「実行結果」は、このファイルをそのまま python3 -m http.server で配信して確認したものです。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>コピーボタン</title>
<style>
.copy-status { margin-left: .5em; font-size: .9em; }
.copy-status[data-state="ok"] { color: #15803d; }
.copy-status[data-state="error"] { color: #b91c1c; }
</style>
</head>
<body>
<p id="order-id">ORD-2026-0001</p>
<button type="button" data-copy-text="#order-id">IDをコピー</button>
<div id="card"><strong>備品番号</strong>:PC-0042<br><a href="https://example.com/items/42">詳細</a></div>
<button type="button" data-copy-html="#card">リッチテキストでコピー</button>
<canvas id="chart" width="120" height="60"></canvas>
<button type="button" data-copy-image="#chart">画像をコピー</button>
<span class="copy-status" role="status" aria-live="polite"></span>
<script type="module">
const statusEl = document.querySelector('.copy-status');
function showStatus(ok, message) {
statusEl.dataset.state = ok ? 'ok' : 'error';
statusEl.textContent = message;
clearTimeout(showStatus.timer);
showStatus.timer = setTimeout(() => { statusEl.textContent = ''; }, 3000);
}
function canUseClipboard() {
return window.isSecureContext && 'clipboard' in navigator;
}
// テキスト
async function copyText(text) {
if (!canUseClipboard()) throw new Error('この環境では Clipboard API が使えません(HTTPS か localhost で開いてください)');
await navigator.clipboard.writeText(text);
}
// HTML + プレーンテキスト(貼り付け先が対応する形式を選ぶ)
async function copyHtml(el) {
if (!canUseClipboard()) throw new Error('この環境では Clipboard API が使えません');
const item = new ClipboardItem({
'text/html': new Blob([el.innerHTML], { type: 'text/html' }),
'text/plain': new Blob([el.innerText], { type: 'text/plain' }),
});
await navigator.clipboard.write([item]);
}
// 画像(PNG)。Blob の Promise をそのまま渡すと、クリック直後に write() を呼べる
async function copyCanvasImage(canvas) {
if (!canUseClipboard()) throw new Error('この環境では Clipboard API が使えません');
if (ClipboardItem.supports && !ClipboardItem.supports('image/png')) {
throw new Error('このブラウザは画像のコピーに対応していません');
}
const pngPromise = new Promise((resolve, reject) =>
canvas.toBlob(b => (b ? resolve(b) : reject(new Error('PNG に変換できません'))), 'image/png'));
await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngPromise })]);
}
document.addEventListener('click', async (e) => {
const btn = e.target.closest('button');
if (!btn) return;
try {
if (btn.dataset.copyText) {
await copyText(document.querySelector(btn.dataset.copyText).textContent.trim());
} else if (btn.dataset.copyHtml) {
await copyHtml(document.querySelector(btn.dataset.copyHtml));
} else if (btn.dataset.copyImage) {
await copyCanvasImage(document.querySelector(btn.dataset.copyImage));
} else {
return;
}
showStatus(true, 'コピーしました');
} catch (err) {
console.error(err);
showStatus(false, `コピーできませんでした:${err.message}`);
}
});
// 見本の画像を描く
const ctx = document.querySelector('#chart').getContext('2d');
ctx.fillStyle = '#2563eb'; ctx.fillRect(10, 20, 30, 40);
ctx.fillStyle = '#16a34a'; ctx.fillRect(50, 10, 30, 50);
</script>
</body>
</html>
設計のポイント
| 箇所 | 理由 |
|---|---|
canUseClipboard() で先に確認 |
HTTP では navigator.clipboard 自体がないため、TypeError ではなく分かりやすい文言で知らせる |
HTML は text/html と text/plain の両方を入れる |
貼り付け先(メモ帳・メール・Word など)が対応する形式を選ぶ。プレーンテキストしか受け付けない場所でも文字が入る |
画像は Blob ではなく Promise<Blob> を渡す |
ClipboardItem は値に Promise を受け取れる(MDN)。重い変換を待たずにクリック直後に write() を呼べるので、ユーザー操作の条件を満たしやすい |
ClipboardItem.supports('image/png') |
対応しない環境では、ボタンを押す前に理由を出せる |
| 状態表示は3秒で消す | 連続して押したときにも最新の結果だけが見える |
実行結果:Playwright でコピーした内容を読み戻す
ボタンをクリックし、コピーされた内容を readText()・read() で読み戻して確認しました。Playwright のブラウザは既定で権限が「prompt(確認待ち)」なので、grantPermissions(['clipboard-read', 'clipboard-write']) で許可してから実行しています。
// test.mjs(Node.js 22.22 / playwright 1.56.1)抜粋
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
const page = await context.newPage();
await page.goto('http://localhost:7160/index.html');
await page.click('[data-copy-text]');
console.log(await page.textContent('.copy-status'));
console.log(await page.evaluate(() => navigator.clipboard.readText()));
await page.click('[data-copy-html]');
console.log(await page.evaluate(async () => {
const [item] = await navigator.clipboard.read();
return { types: item.types,
html: await (await item.getType('text/html')).text(),
plain: await (await item.getType('text/plain')).text() };
}));
await page.click('[data-copy-image]');
console.log(await page.evaluate(async () => {
const [item] = await navigator.clipboard.read();
const blob = await item.getType('image/png');
const bmp = await createImageBitmap(blob);
return { types: item.types, size: blob.size, w: bmp.width, h: bmp.height };
}));
実行結果(Chromium 141.0.7390.37、見やすさのため改行を入れています):
コピーしました
ORD-2026-0001
{ types: ["text/plain","text/html"],
html: "<strong>備品番号</strong>:PC-0042<br><a href=\"https://example.com/items/42\">詳細</a>",
plain: "備品番号:PC-0042\n詳細" }
{ types: ["image/png"], size: 410, w: 120, h: 60 }
テキスト・HTML・画像のどれも、ボタンで書いた内容がそのまま読み戻せました。画像は 120×60 の PNG(410 バイト)として入っており、canvas のサイズと一致しています。
同じページで ClipboardItem.supports() を調べた結果です。
| 形式 | Chromium 141 の結果 |
|---|---|
text/plain |
true |
text/html |
true |
image/png |
true |
image/jpeg |
false |
image/svg+xml |
true |
web text/custom(独自形式) |
true |
JPEG は false でした。写真をコピーしたいときは、いったん canvas に描いて toBlob(..., 'image/png') で PNG にしてから書き込むと確実です。web で始まる独自形式(Web カスタムフォーマット)は、同じブラウザ上の Web アプリ間でデータを受け渡す用途に使えます。
画像ファイル(URL)をコピーする
表示中の画像をコピーしたいときは、fetch した Blob の Promise を ClipboardItem に渡します。PNG 以外の画像は canvas で PNG に変換します。
async function copyImageFromUrl(url) {
const pngPromise = (async () => {
const res = await fetch(url); // 同じオリジン、または CORS 許可のある画像
const blob = await res.blob();
if (blob.type === 'image/png') return blob;
const bmp = await createImageBitmap(blob); // JPEG・WebP などを PNG に変換
const canvas = new OffscreenCanvas(bmp.width, bmp.height);
canvas.getContext('2d').drawImage(bmp, 0, 0);
return canvas.convertToBlob({ type: 'image/png' });
})();
await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngPromise })]);
}
document.querySelector('#copy-photo').addEventListener('click', () =>
copyImageFromUrl('/images/item-42.jpg').catch(err => alert(err.message)));
別オリジンの画像で CORS 許可がない場合は fetch が失敗し、canvas に描いても書き出せません。コピー対象の画像は、自分のサイトから配信するか CORS を許可しておくと扱いやすくなります。
execCommand('copy') へのフォールバックについて
document.execCommand('copy') は MDN で非推奨(deprecated)と書かれており、新しく書くコードでは Clipboard API を使うことが推奨されています。今回の検証でも、HTTP のページで textarea を選択して execCommand('copy') を呼ぶと true が返りました。つまり「HTTP でも動くから」とフォールバックに入れたくなりますが、次の理由でおすすめしません。
| 観点 | Clipboard API | execCommand('copy') |
|---|---|---|
| 仕様上の位置づけ | 現行の標準 | 非推奨 |
| 結果の受け取り | Promise(失敗理由が分かる) | true / false のみ |
| 画像・HTML | ClipboardItem で扱える |
選択範囲に依存 |
| 副作用 | なし | 一時的な textarea の追加・選択範囲の変更・フォーカス移動 |
HTTP で動かない場合は、フォールバックを足すより HTTPS で配信する(開発中は localhost を使う)ほうが確実です。どうしても対応できない環境では、テキストを選択状態にして「Ctrl+C でコピーしてください」と表示する方法が、利用者にとって分かりやすい代替になります。
動かない原因一覧と実際のエラー文
動かないときは、まずブラウザのコンソールに出るエラー名を見ます。TypeError なら API がない(HTTP など)、NotAllowedError なら条件(権限・操作・iframe・フォーカス)が足りない、というように切り分けられます。
| 症状・エラー | 原因 | 確認方法 | 直し方 |
|---|---|---|---|
TypeError: Cannot read properties of undefined (reading 'writeText') |
HTTP(localhost 以外)で開いている | isSecureContext が false |
HTTPS で配信する。開発中は localhost で開く |
NotAllowedError: ... Write permission denied. |
ユーザー操作の外で呼んでいる/権限が拒否されている | 呼び出し位置がクリックハンドラーの中か | クリックの中で呼ぶ。Playwright などのテストでは権限を付与する |
NotAllowedError: ... Read permission denied. |
読み取りの許可がない | navigator.permissions.query({ name: 'clipboard-read' }) |
読み取りはボタン操作から呼び、許可を求める。貼り付けは paste イベントでも受け取れる |
NotAllowedError: ... blocked because of a permissions policy ... |
別オリジンの iframe に許可がない | iframe の allow 属性 |
親ページで <iframe allow="clipboard-write">(読み取りは clipboard-read)を付ける。Permissions-Policy ヘッダーで禁止していないかも確認 |
NotAllowedError: Document is not focused. |
ページにフォーカスがない(DevTools のコンソールから実行した直後など) | document.hasFocus() |
ボタンのクリックから実行する。コンソールで試すときは setTimeout で遅らせてページをクリックする |
| 画像のコピーだけ失敗する | 形式が未対応(JPEG など)/重い処理の後に write() を呼んでいる |
ClipboardItem.supports(type) |
PNG に変換する。Promise<Blob> を渡してクリック直後に write() を呼ぶ |
| Safari・Firefox で操作なしの書き込みが失敗 | この2つは clipboard-write 権限に対応せず、書き込みにユーザー操作が必要(MDN) |
呼び出しが setTimeout・通信完了後などになっていないか |
クリックの中で write() を呼び、データは Promise で渡す |
| 読み取り時に「ペースト」メニューが出る | Firefox・Safari の仕様。同一オリジンのデータなら出ない(MDN) | ― | 利用者に押してもらう前提で画面を作る |
| Web Worker で使えない | Worker には navigator.clipboard がない(MDN) |
― | メインスレッドでコピーし、Worker からはデータだけ受け取る |
実行結果:失敗するパターンを再現
ここからは、上の表のエラー文を Playwright(Chromium 141)で実際に出したものです。HTTP の確認には、--host-resolver-rules=MAP example.test 127.0.0.1 で localhost 以外のホスト名を同じサーバーに向けています。
| No | 条件 | 結果(そのまま転記) |
|---|---|---|
| 1 | http://example.test:7160(HTTP・localhost 以外) |
{"isSecureContext":false,"hasClipboard":false,"typeofCI":"undefined"} → TypeError: Cannot read properties of undefined (reading 'writeText') |
| 2 | 同上で完成版ボタンを押す | コピーできませんでした:この環境では Clipboard API が使えません(HTTPS か localhost で開いてください) |
| 3 | 権限の付与なし・操作なしで writeText |
NotAllowedError: Failed to execute 'writeText' on 'Clipboard': Write permission denied. |
| 4 | 権限の付与なし・操作なしで readText |
NotAllowedError: Failed to execute 'readText' on 'Clipboard': Read permission denied. |
| 5 | 権限の付与なしでボタンをクリック(Playwright の既定状態) | コピーできませんでした:Failed to execute 'writeText' on 'Clipboard': Write permission denied. |
| 6 | clipboard-write だけ付与・操作なしで writeText |
成功(ok) |
| 7 | 別オリジン iframe・allow なし |
Failed to execute 'writeText' on 'Clipboard': The Clipboard API has been blocked because of a permissions policy applied to the current document. See https://crbug.com/414348233 for more details. |
| 8 | 別オリジン iframe・allow="clipboard-read; clipboard-write" |
コピーしました |
| 9 | HTTP ページで execCommand('copy') |
true |
No.1〜2 から、HTTP では API そのものが存在しないことが分かります。完成版のように isSecureContext を先に確認しておくと、利用者には意味の分かる文言を出せます。
No.5 は、自動テストでつまずきやすい点です。Playwright が作るブラウザコンテキストでは navigator.permissions.query の結果が clipboard-read: prompt、clipboard-write: prompt で、確認ダイアログに答える人がいないため、クリックからでも書き込みが拒否されました。E2E テストでは grantPermissions で権限を与えてから実行します。clipboard-write だけを付与すると clipboard-read は denied になり、読み戻しのテストができないので、両方を付与します。
No.7〜8 のとおり、iframe は allow 属性の有無で結果が変わります。親ページ(localhost:7160)から別オリジン(localhost:7161)の iframe を読み込み、同じボタンを押して比較しました。
<!-- 親ページ -->
<iframe src="http://localhost:7161/index.html"></iframe> <!-- 失敗 -->
<iframe src="http://localhost:7161/index.html" allow="clipboard-read; clipboard-write"></iframe> <!-- 成功 -->
フォーカスの条件(Document is not focused.)は、ヘッドレスの Chromium ではページが常にフォーカスを持つ扱いになり(別タブを前面にしても document.hasFocus() が true)、今回は再現できませんでした。DevTools のコンソールから直接 navigator.clipboard.writeText() を実行するとフォーカスがコンソール側にあるため、この形のエラーになることがあります。試すときは次のように遅らせて、その間にページをクリックします。
setTimeout(() => navigator.clipboard.writeText('テスト').then(() => console.log('ok'), console.error), 3000);
React でコピーボタンを作る
React では、コピー処理と「コピーしました」の表示をカスタムフックにまとめると、どのコンポーネントからも使えます。下のコードは React 19 + TypeScript の想定で書いたもので、この記事では実行していません(ブラウザ側の動作は上の完成版で検証済みです)。
// useCopyToClipboard.ts
import { useCallback, useEffect, useRef, useState } from 'react';
type CopyState = 'idle' | 'copied' | 'error';
export function useCopyToClipboard(resetMs = 2000) {
const [state, setState] = useState<CopyState>('idle');
const [error, setError] = useState<string | null>(null);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
const copy = useCallback(async (data: string | ClipboardItem) => {
if (timer.current) clearTimeout(timer.current);
try {
if (!window.isSecureContext || !navigator.clipboard) {
throw new Error('HTTPS か localhost で開いてください');
}
if (typeof data === 'string') {
await navigator.clipboard.writeText(data);
} else {
await navigator.clipboard.write([data]);
}
setState('copied');
setError(null);
} catch (e) {
setState('error');
setError(e instanceof Error ? e.message : String(e));
}
timer.current = setTimeout(() => setState('idle'), resetMs);
}, [resetMs]);
return { copy, state, error };
}
// CopyButton.tsx
import { useCopyToClipboard } from './useCopyToClipboard';
export function CopyButton({ text }: { text: string }) {
const { copy, state, error } = useCopyToClipboard();
return (
<>
<button type="button" onClick={() => copy(text)}>
{state === 'copied' ? 'コピーしました' : 'コピー'}
</button>
<span role="status" aria-live="polite">
{state === 'error' ? `コピーできませんでした:${error}` : ''}
</span>
</>
);
}
React で気をつける点
| 点 | 理由と対策 |
|---|---|
onClick の中で copy() を呼ぶ |
useEffect の中で自動コピーするとユーザー操作の外になり、NotAllowedError になる |
copy() の前に await を挟まない |
通信などを待ってから書き込むと、操作の有効期間を過ぎることがある。データ取得が必要なら ClipboardItem に Promise を渡す |
| タイマーをアンマウント時に解除 | 画面遷移の後に state を更新しない |
| Next.js などの SSR | navigator はブラウザにしかないので、イベントハンドラーやクライアントコンポーネント('use client')の中で参照する |
貼り付け(読み取り)を扱うときの注意
利用者が貼り付けた内容を受け取るだけなら、navigator.clipboard.read() より paste イベントのほうが向いています。paste イベントは利用者の Ctrl+V(右クリックの貼り付け)で発生するため、権限の確認が出ず、HTTP のページでも動きます。
const memo = document.querySelector('#memo'); // <textarea id="memo">
memo.addEventListener('paste', (e) => {
const data = e.clipboardData; // DataTransfer
// 画像が貼り付けられたとき
const images = [...data.files].filter(f => f.type.startsWith('image/'));
if (images.length > 0) {
e.preventDefault(); // 既定の貼り付けを止める(同期的に呼ぶ)
images.forEach(uploadImage); // 自前の処理へ
return;
}
// テキストは整形して入れる例(前後の空白を除く)
const text = data.getData('text/plain');
e.preventDefault();
memo.setRangeText(text.trim(), memo.selectionStart, memo.selectionEnd, 'end');
});
function uploadImage(file) {
console.log('画像を受け取りました', file.type, file.size);
}
実行結果:paste イベントで受け取れる内容
完成版の「リッチテキストでコピー」と「画像をコピー」を押した後、textarea で Ctrl+V を押し、e.clipboardData の中身を記録しました(Chromium 141)。
[{"types":["text/plain","text/html"],"text":"備品番号:PC-0042\n詳細","html":"<html><head></head><body><strong>備品番号</s","files":[]},
{"types":["Files"],"text":"","html":"","files":["image/png 410B"]}]
画像は getData() ではなく files に File として入ります。また、paste イベントで取り出した text/html は <html><head></head><body>... で包まれた形になっていました(read() で読んだときは包まれていませんでした)。HTML を使うときは、包みの有無に関係なく扱えるように DOMParser で解析してから必要な部分を取り出すと安定します。
読み取りで気をつけること
| 項目 | 内容 |
|---|---|
preventDefault() は同期的に呼ぶ |
await の後で呼んでも、既定の貼り付けはすでに始まっている |
貼り付けられた HTML をそのまま innerHTML に入れない |
スクリプトやイベント属性が含まれることがある。テキストとして扱うか、許可したタグだけを残すサニタイズを通す |
navigator.clipboard.readText() は確認が出る前提で |
Chromium は clipboard-read の許可、Firefox・Safari は「ペースト」メニューの操作が必要(MDN) |
| 読み取りはユーザーの意図がはっきりした場面だけ | 「貼り付け」ボタンなど、利用者が自分で押したときに限ると、確認が出ても戸惑われにくい |
Playwright でクリップボードをテストする書き方
コピーボタンの E2E テストは「権限を付与 → クリック → 読み戻して比較」の3ステップです。Playwright Test で書くと次のようになります(この形は上の検証スクリプトと同じ手順です)。
// tests/copy.spec.ts
import { test, expect } from '@playwright/test';
test.use({ permissions: ['clipboard-read', 'clipboard-write'] });
test('IDをコピーできる', async ({ page }) => {
await page.goto('http://localhost:7160/index.html');
await page.getByRole('button', { name: 'IDをコピー' }).click();
await expect(page.getByRole('status')).toHaveText('コピーしました');
const copied = await page.evaluate(() => navigator.clipboard.readText());
expect(copied).toBe('ORD-2026-0001');
});
permissions の付与は Chromium 向けの方法です。Firefox・Safari は clipboard-read・clipboard-write 権限に対応していない(MDN)ため、ブラウザごとにテストの組み方を分けます。
よくある質問
Q. HTTP のサイトでどうしてもコピーボタンを使いたいです。どうすればよいですか?
A. HTTP(localhost 以外)では navigator.clipboard 自体がありません。HTTPS で配信するのがいちばん確実です。開発中は http://localhost で開けば安全なコンテキストとして扱われます。execCommand('copy') は HTTP でも true を返しましたが、MDN で非推奨とされているため、新しいコードの主な手段にはしないほうが安心です。
Q. ボタンを押してから API で文字列を取得し、その後でコピーすると失敗します。
A. 通信を待つ間に、ユーザー操作の有効期間を過ぎることがあります。ClipboardItem の値には Promise を渡せるので、クリック直後に write() を呼び、データは Promise で後から渡します。
button.addEventListener('click', () => {
const textPromise = fetch('/api/share-url').then(r => r.text())
.then(t => new Blob([t], { type: 'text/plain' }));
navigator.clipboard.write([new ClipboardItem({ 'text/plain': textPromise })])
.then(() => showStatus(true, 'コピーしました'), err => showStatus(false, err.message));
});
Q. 画像をコピーしたのに、貼り付け先で画像として入りません。
A. 書き込んだ形式が image/png か確認してください。今回の Chromium 141 では ClipboardItem.supports('image/jpeg') が false でした。JPEG や WebP は canvas で PNG に変換してから書き込みます。貼り付け先のアプリが画像の貼り付けに対応しているかも確認します。
Q. コピーしたことを利用者にどう伝えるのがよいですか?
A. ボタンの文言を一時的に「コピーしました」に変える、または role="status" の要素に結果を出す方法が分かりやすいです。失敗時はエラーの message を添えると、HTTP・iframe・権限のどれが原因か利用者や開発者が判断できます。
まとめ
- テキストは
navigator.clipboard.writeText()、HTML・画像はwrite()+ClipboardItem。どちらもawaitしてtry/catchで結果を表示する - 動く条件は 安全なコンテキスト(HTTPS・localhost)、ユーザー操作、権限、iframe の
allow、フォーカス の5つ - HTTP では
TypeError: Cannot read properties of undefined (reading 'writeText')、条件不足はNotAllowedErrorと、エラー名で原因を切り分けられる - 画像は PNG で書き込み、
Promise<Blob>を渡してクリック直後にwrite()を呼ぶ。Chromium 141 では JPEG は非対応だった execCommand('copy')は 非推奨。HTTP 対策はフォールバックより HTTPS 化 が確実- 貼り付けの受け取りは paste イベントの
clipboardDataが扱いやすく、画像はfilesに入る - E2E テストでは
clipboard-readとclipboard-writeの両方を付与 し、コピー内容を読み戻して確認する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptクリップボードコピー|画像対応・動かない原因一覧」
株式会社RJCでは、ブラウザ API の動きを手元で確かめる習慣を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。