技術資料

Clipboard APIの落とし穴:localhost・HTTPS・ユーザー操作の違い

作成日:2026.09.16

navigator.clipboard.writeText()によるクリップボードへの自動コピーが、localhostでは動くのにLAN内IPのHTTPアクセスでは失敗する理由を、Secure Context、ユーザー操作、ブラウザ権限の観点から整理します。HTTPS化の考え方、JavaScriptによる確認方法、失敗時に手動コピーへ切り替える実装例、document.execCommand('copy')を使う場合の注意点も紹介します。

Webアプリケーションで、画面に表示した文字列をクリップボードへ自動的にコピーする処理を実装した際につまずきがちな現象として、同じJavaScriptでも、http://localhostでは動くのに、http://192.168.0.x では動かないことがあります。また、画面を開いた直後の自動コピーは失敗するのに、ボタンをクリックするとコピーできる場合もあります。

このような挙動は、JavaScriptのコードだけではなく、Clipboard APIのSecure Context、ユーザー操作、ブラウザの権限や実装差によって決まります。今回は、ブラウザ標準のJavaScriptだけを使い、クリップボードへの自動コピーが失敗するケースの整理や、失敗した場合に手動コピーへ切り替える方法を整理します。

前提

この記事では、次のAPIを使います。

  • navigator.clipboard.writeText()によるテキストの書き込み
  • window.isSecureContextによるSecure Contextの確認
  • 自動コピーに失敗した場合の手動コピー用ボタン

Clipboard APIの挙動はブラウザやバージョンによって変わる可能性があります。ここで説明する条件は、公開前に対象ブラウザで確認する前提です。

Clipboard APIで文字列をコピーする

テキストをクリップボードへ書き込む場合は、navigator.clipboard.writeText()を使います。

async function copyText(text) {
    await navigator.clipboard.writeText(text);
}

copyText('コピーする文字列');

writeText()はPromiseを返します。

Promiseは、すぐには結果が返らない非同期処理の完了を、後から受け取るためのオブジェクトです。クリップボードへのコピー処理が成功して完了し、awaitの次の処理へ進める状態になると、Promiseが解決されます。許可されていない場合など、処理に失敗するとPromiseはrejectされ、try...catchなどで失敗として扱えます。

実際の画面では、APIが存在しない場合や例外が発生する場合を考慮して、次のように成功・失敗を呼び出し元へ返す形にしておくと扱いやすくなります。

async function tryCopy(text) {
    if (!window.isSecureContext) {
        return false;
    }

    if (!navigator.clipboard?.writeText) {
        return false;
    }

    try {
        await navigator.clipboard.writeText(text);
        return true;
    } catch (error) {
        console.warn('clipboard write failed', error);
        return false;
    }
}

ここでは、Secure Contextでない場合とClipboard APIが利用できない場合を、コピー失敗として扱っています。例外の内容は原因調査には役立ちますが、利用者へそのまま表示するのではなく、画面には復旧方法を案内する方がよいでしょう。

Clipboard APIにはSecure Contextが必要

Clipboard APIは、基本的にSecure Contextで利用するAPIです。Secure Contextは、HTTPSやループバックアドレスなど、一定の条件を満たしたWebページの実行環境を指します。つまり、ブラウザが安全な通信経路、または信頼できるローカル環境から提供されていると判断し、強力なWeb APIを利用してよいと認めた状態です。

例えば、次のようなURLは同じHTTPでも扱いが異なります。

URLの例 確認時の考え方
https://example.com HTTPSなのでSecure Contextになり得る
http://localhost 開発用途のlocalhostとしてSecure Contextになり得る
http://127.0.0.1 ループバックアドレスとして扱われる
http://192.168.0.2 LAN内のプライベートIPだが、localhostとは別の扱いになる

LAN内のプライベートIPは、インターネットから直接アクセスしにくいアドレスという意味ではあります。しかし、ループバックアドレスではないため、HTTPでアクセスしたときにlocalhostと同じ例外が適用されるとは限りません。

Secure Contextかどうかは、ブラウザの開発者ツールで次の値を確認できます。

console.log(window.isSecureContext);

trueならSecure Contextです。falseなら、Clipboard APIを使う前にHTTPSやアクセス先のURLを確認します。ただし、trueであっても、権限やユーザー操作の条件を満たしているとは限りません。

LAN内の別端末から使う場合はHTTPSを検討する

同じPCで開発画面を確認するだけなら、http://localhostを使う方法があります。

スマートフォンや別のPCからLAN内の開発サーバーへアクセスする場合は、例えば次のようなHTTPSの構成を検討します。

https://192.168.0.2

単にURLをhttpsへ変更するだけではなく、アクセス先のIPアドレスを含む証明書を用意し、アクセスする端末でその証明書またはローカルCAを信頼させる必要があります。証明書エラーが表示された状態では、Secure Contextとして正しく確認できないことがあります。

ローカルHTTPSや証明書の作成方法は、開発環境やOSによって異なります。利用する開発サーバーや証明書ツールの公式資料に合わせて設定してください。

ユーザー操作の直後かどうかで挙動が変わる

Secure Contextであることに加えて、クリップボードへ書き込む処理をユーザー操作の直後に実行しているかどうかも重要です。

例えば、ボタンのクリックを起点にする処理は、ユーザー操作と直接つながっています。

const copyButton = document.querySelector('#copy-button');

copyButton.addEventListener('click', async () => {
    const copied = await tryCopy('ボタンからコピーする文字列');

    if (copied) {
        console.log('コピーしました');
    } else {
        console.log('コピーできませんでした');
    }
});

一方、次のような処理は、ページの読み込みや状態変化を起点にしているため、ユーザー操作と直接つながっていません。

window.addEventListener('load', async () => {
    const copied = await tryCopy('ページ表示時にコピーする文字列');

    if (!copied) {
        console.log('自動コピーできませんでした');
    }
});

ブラウザは、利用者が意図しないタイミングでページからクリップボードを書き換えられないように、ユーザー操作や権限を確認します。クリック、キー入力、タップなどの直後に一時的に成立する状態を、transient user activationと呼びます。

Clipboard APIの書き込み条件はブラウザ間で完全に同じではありません。Chromium系では、clipboard-write権限またはtransient user activationのどちらかが必要になる場合があります。FirefoxやSafariでは、ユーザー操作が必須になる場合があります。

このため、Chromeでページ表示時の自動コピーが成功しても、FirefoxやSafariで同じ結果になるとは限りません。特定のブラウザの挙動を前提にする場合は、対象ブラウザとバージョンを記事やシステムの前提として明記する必要があるかもしれません。

iframeやページの状態も確認する

ページをiframe内で表示している場合は、親ページとiframeの両方がSecure Contextである必要があります。また、Permissions Policyでクリップボード操作が許可されていなければ、iframe内の処理が失敗することがあります。

例えば、iframeを埋め込む側で次のような許可を指定する構成があります。

<iframe
    src="https://app.example.com"
    allow="clipboard-write"
></iframe>

実際に許可するオリジンやHTTPレスポンスヘッダーの設定は、埋め込み元と埋め込み先の構成に合わせて決めます。iframeを使っていない場合でも、対象ページがブラウザでフォーカスされているか、サイトの権限設定でクリップボードが拒否されていないか確認が必要です。

自動コピーに失敗したら手動コピーを案内する

自動コピーを業務処理の必須条件にすると、ブラウザの制約によって利用者が先へ進めなくなることがあります。自動コピーは便利な補助機能と考え、失敗した場合は文字列を画面へ表示し、利用者がボタンを押して再試行できるようにするのが良いかと思います。

例えば、次のようなHTMLを用意します。

<section id="copy-area" hidden>
    <p>自動コピーできませんでした。次のボタンを押してください。</p>
    <button type="button" id="copy-button">コピー</button>
    <p id="copy-status" aria-live="polite"></p>
</section>

自動コピーの結果に応じて、成功通知または手動コピー用の領域を表示します。

const textToCopy = '画面に表示した文字列';
const copyArea = document.querySelector('#copy-area');
const copyButton = document.querySelector('#copy-button');
const copyStatus = document.querySelector('#copy-status');

async function copyAutomatically() {
    const copied = await tryCopy(textToCopy);

    if (copied) {
        copyStatus.textContent = 'クリップボードへコピーしました。';
        return;
    }

    copyArea.hidden = false;
    copyStatus.textContent = '自動コピーできませんでした。';
}

copyButton.addEventListener('click', async () => {
    const copied = await tryCopy(textToCopy);

    copyStatus.textContent = copied
        ? 'クリップボードへコピーしました。'
        : 'コピーできませんでした。文字列を選択してコピーしてください。';
});

copyAutomatically();

この例では、ボタンをクリックした場合も、まずClipboard APIを使います。ボタン操作によってユーザー操作の条件を満たしやすくなるため、自動実行時には失敗した処理が成功する可能性があります。

それでも失敗する場合に備えて、コピー対象の文字列そのものを画面へ表示しておくと、利用者はブラウザ標準の選択・コピー操作で作業を続けられます。

document.execCommand('copy')をフォールバックにする

Clipboard APIが使えない環境では、以前から使われてきたdocument.execCommand('copy')を、ユーザーがボタンを押したときだけ試す方法があります。

function copyWithExecCommand(text) {
    const textarea = document.createElement('textarea');

    textarea.value = text;
    textarea.setAttribute('readonly', '');
    textarea.style.position = 'fixed';
    textarea.style.left = '-9999px';

    document.body.appendChild(textarea);
    textarea.select();
    textarea.setSelectionRange(0, textarea.value.length);

    let copied = false;

    try {
        copied = document.execCommand('copy');
    } finally {
        textarea.remove();
    }

    return copied;
}

execCommand()は非推奨で、ブラウザによって動作条件が異なります。新規実装の基本方式にはせず、Clipboard APIが使えない場合の限定的なフォールバックとして、対象ブラウザで動作を確認した上で使うに留めましょう。

また、execCommand('copy')を使っても、Secure Contextやユーザー操作などの制約をすべて回避できるわけではありません。自動コピー制限を無理に突破する方法ではなく、利用者のクリックを起点にした再試行の手段として扱うのが良いと思います。

Clipboard APIとexecCommand()を組み合わせる場合は、次のようにボタン操作の中でだけフォールバックを実行します。

copyButton.addEventListener('click', async () => {
    let copied = await tryCopy(textToCopy);

    if (!copied) {
        copied = copyWithExecCommand(textToCopy);
    }

    copyStatus.textContent = copied
        ? 'クリップボードへコピーしました。'
        : 'コピーできませんでした。文字列を選択してコピーしてください。';
});

古いAPIを使う場合も、戻り値を確認し、失敗したときの表示を用意します。

動作しないときの確認手順

クリップボードへのコピーが動かない場合は、次の順番で確認すると原因を切り分けやすくなります。

1. URLとSecure Contextを確認する
console.log(location.href);
console.log(window.isSecureContext);

http://localhostでは動くのに、http://192.168.x.xではfalseになるような場合は、アクセス先がSecure Contextとして扱われているかを確認します。

2. Clipboard APIの存在を確認する
console.log(navigator.clipboard);
console.log(typeof navigator.clipboard?.writeText);

APIが存在しない場合は、URLの条件や対象ブラウザを確認します。

3. 例外の名前とメッセージを確認する
try {
    await navigator.clipboard.writeText('test');
} catch (error) {
    console.error(error.name, error.message);
}

NotAllowedErrorが発生した場合は、Secure Context、ユーザー操作、権限、iframeのポリシーなどを確認します。

4. ユーザー操作の直後か確認する

ページ表示時、loadイベント、タイマー、状態変更を起点にした場合だけ失敗し、ボタン操作では成功するなら、transient user activationや権限が関係している可能性があります。

対応ブラウザでは、次の値を補助的に確認できます。

console.log(navigator.userActivation?.isActive);

この値だけでClipboard APIの利用可否を判定するのではなく、実際のwriteText()の結果と合わせて確認します。

5. ブラウザとサイトの権限を確認する

ブラウザのサイト情報や権限設定で、クリップボードへのアクセスが拒否されていないかを確認します。会社や学校の管理ポリシーによって、利用者が変更できない場合もあります。

自動コピーを実装するときの注意点

  • 自動コピーの成功を前提に、次の処理へ進めない。
  • コピー対象の文字列を画面にも表示し、手動操作で復旧できるようにする。
  • 成功時は「何をコピーしたか」を利用者へ通知する。
  • 失敗時は、原因の詳細よりも「コピー」ボタンなどの復旧手段を表示する。
  • ユーザー操作なしの書き込みを必須にする場合は、対象ブラウザと権限設定を限定する。
  • クリップボードへ機密情報を書き込む場合は、表示・保存・利用後の消去まで含めて扱いを検討する。
  • iframeを使う場合は、親ページ、埋め込みページ、Permissions Policyを確認する。

まとめ

navigator.clipboard.writeText()でクリップボードへ書き込むには、Secure Contextやブラウザの権限など、JavaScriptのコード以外の条件も関係します。

http://localhostは開発用途でSecure Contextとして扱われる場合がありますが、http://192.168.x.xのようなLAN内IPのHTTPアクセスが同じ扱いになるとは限りません。別端末から確認する場合は、信頼できる証明書を使ったHTTPSを検討します。

また、HTTPSでも、ページ表示時の自動コピーとボタン操作によるコピーで結果が異なることがあります。自動コピーが失敗した場合は、手動コピー用ボタンと文字列表示を用意し、利用者が作業を続けられるようにします。

document.execCommand('copy')は非推奨のため、基本はClipboard APIを使います。必要な場合だけ、ユーザー操作を起点にした限定的なフォールバックとして検討し、対象ブラウザで動作を確認してから採用します。

参考資料

この記事を書いた人

※上が私です。

奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。

これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。

【制作会社・企業様向けサポート】
  • 専任エンジニアのいない企業様に対するシステム面の不安を解消
  • 柔軟な契約形態や短納期での対応により、急なニーズにも迅速にサポート
  • システムの企画段階から運用まで、ワンストップでのサービスを提供

Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。

ぜひ、プロフィールWeb制作会社様向け業務案内一般企業様向け業務案内もご覧くださいね。

自動取得後の大量データ確認を一日半から2時間に短縮した、ワークキュー型UIの設計

2026.09.12

自動取得後の大量データの確認・補正作業を題材に、管理画面をワークキュー型UIへ改善した事例を紹介します。少数件のキュー、自動補充、解決済み項目の非入力化、例外の別レーン化、unknown_resultや楽観的ロックなど、人間が大量データを安全かつ効率的に処理するための設計をまとめます。

JavaScript UI設計 Vue.js

ViteのJavaScriptチャンクサイズ警告が出たときの判断基準:コード分割・閾値変更・現状維持

2026.09.08

ViteのビルドでJavaScriptチャンクサイズ警告が出たときに、ビルドエラーとの違い、非圧縮サイズとgzipサイズの見方、静的importによるチャンク肥大化の原因を整理する。動的importや依存ライブラリの見直し、警告閾値の変更、現状維持について、一般ユーザー向けサービス・社内管理画面・自分専用の管理画面ごとの判断基準と本番運用上の注意点を紹介する。

JavaScript Vite Vue.js

Vue.jsで一覧画面の無限スクロールを実装する:サンプルJSONから一覧データを取得

2026.09.03

Vue.js単体のViteプロジェクトで、サンプルJSONからデータを取得し、IntersectionObserverで画面下部の監視要素を検知して5件ずつカード表示する無限スクロールを実装します。fetchのエラー処理や再読み込み、全件表示後の監視解除も扱い、サンプルJSONを一括取得するモックと、実際のAPIによるページングの違いも説明します。

JavaScript Vue.js

Laravel 13+Vue.jsで画像・CSVを非同期アップロードする:FormData・ファイル検証・非公開保存

2026.08.31

Laravel 13のBladeに組み込んだVue.jsから、FormDataを使って画像またはCSVを非同期で送信する方法を解説します。JPEG・PNGの画像検証、CSVの文字コード変換と簡易的な内容確認、20MBのサイズ制限、非公開ディスクへの保存、CSRF・バリデーションエラー処理を実装し、fetchを基本にaxiosとの違いも整理します。

JavaScript Laravel PHP Vue.js

阿部辰也へのお仕事の依頼・お問い合わせ

軽いご相談もお気軽にどうぞ!

個人情報の取り扱いについて *必須 プライバシーポリシーをご確認いただき、同意いただける場合は「同意する」にチェックをしてください。

keyboard_double_arrow_up
TOP