【JavaScript】コピーボタン(Clipboard API)を実装する

javascript-icon JavaScript

こんにちは、かつコーチです。

前回は無限スクロールを扱いました。

今回は「実践UI実装」シリーズの最終回として、クーポンコードやシェア用URLをワンクリックでコピーできるコピーボタンを実装します。

初心者向けのテーマなので、これまでよりもシンプルな内容から丁寧に見ていきましょう。

コピーボタンとは?

Clipboard APIとは

Clipboard APIは、ブラウザからOSのクリップボード(コピー&ペーストの一時保管場所)を操作するための標準機能です。

navigator.clipboard.writeText()というメソッドを呼ぶだけで、指定した文字列をクリップボードにコピーできます。

以前はdocument.execCommand("copy")という古い方法が使われていましたが、現在は非推奨とされており、Clipboard APIを使うのが標準的な方法です。

どんな場面で使われるか

代表的な使いどころは次のようなものです。

  • クーポンコードやプロモコードのコピー
  • SNSシェア用のURLコピー
  • APIキーやコマンドなど、長い文字列のコピー

「範囲選択してCtrl+C」という手間をなくし、ボタン1つでコピーを完結させることで、ユーザーの離脱を防ぐ効果があります。

基本の実装手順

手順1:HTMLとCSSでコピー対象とボタンを用意する

まずはコピーしたいテキストと、ボタンを用意します。

<div class="copy-box">
  <input type="text" id="coupon-code" value="WELCOME2026" readonly />
  <button id="copy-btn" class="copy-btn">コピー</button>
</div>
.copy-box {
  display: flex;
  gap: 8px;
  max-width: 320px;
}
#coupon-code {
  flex: 1;
  padding: 8px 10px;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  font-size: 1rem;
}
.copy-btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background: #2563eb;
  color: #fff;
  cursor: pointer;
}
.copy-btn.copied {
  background: #16a34a;
}

inputにはreadonlyを付けておき、ユーザーが誤って中身を書き換えられないようにしています。

手順2:Clipboard APIでコピー処理を書く

続いて、ボタンを押したときの処理を書いていきます。

const copyBtn = document.getElementById("copy-btn");
const couponInput = document.getElementById("coupon-code");

copyBtn.addEventListener("click", async () => {
  try {
    await navigator.clipboard.writeText(couponInput.value);
    copyBtn.textContent = "コピーしました!";
    copyBtn.classList.add("copied");
  } catch (err) {
    copyBtn.textContent = "コピーに失敗しました";
    console.error("クリップボードへのコピーに失敗:", err);
  }
});

navigator.clipboard.writeText()は非同期処理(Promise:処理の完了を後から受け取れる仕組み)なので、awaitを使って完了を待ってからボタンの見た目を変えています。

コピーが成功したらボタンの文字と色を変えて、ユーザーに「ちゃんとコピーできましたよ」と伝えるのがポイントです。

手順3:一定時間後に表示を元に戻す

コピー成功の表示を出しっぱなしにせず、数秒後に元のボタン表示へ戻します。

copyBtn.addEventListener("click", async () => {
  try {
    await navigator.clipboard.writeText(couponInput.value);
    copyBtn.textContent = "コピーしました!";
    copyBtn.classList.add("copied");

    setTimeout(() => {
      copyBtn.textContent = "コピー";
      copyBtn.classList.remove("copied");
    }, 2000);
  } catch (err) {
    console.error("クリップボードへのコピーに失敗:", err);
  }
});

2秒後に文字と色を元に戻すことで、ボタンが「押せる状態」に戻ったことも視覚的に伝わります。

よくあるつまずきポイント・エラー対処

つまずき:ローカルの動作確認では動くのに本番環境で急に動かなくなった

私が最初にこの機能を実装したとき、localhostでの動作確認では問題なくコピーできていたのに、本番にアップロードした途端に動かなくなり、かなり焦った経験があります。

❌ Before:HTTP環境でも動く前提でコードを書いていた

copyBtn.addEventListener("click", () => {
  navigator.clipboard.writeText(couponInput.value);
  // エラーハンドリングがなく、失敗しても気づけない
});

コンソールを確認すると、navigator.clipboard is undefinedというエラーが出ていました。

原因は、Clipboard APIがHTTPS環境(またはlocalhost)でしか使えない仕様になっていたことでした。

当時デプロイしていたテスト環境がまだHTTP接続だったため、navigator.clipboardオブジェクト自体が存在せず、writeTextを呼ぼうとしてエラーになっていたのです。

✅ After:Clipboard APIが使えない場合の対処も含めて書く

copyBtn.addEventListener("click", async () => {
  if (!navigator.clipboard) {
    alert("お使いの環境ではコピー機能を利用できません(HTTPS環境が必要です)");
    return;
  }

  try {
    await navigator.clipboard.writeText(couponInput.value);
    copyBtn.textContent = "コピーしました!";
    copyBtn.classList.add("copied");
    setTimeout(() => {
      copyBtn.textContent = "コピー";
      copyBtn.classList.remove("copied");
    }, 2000);
  } catch (err) {
    console.error("クリップボードへのコピーに失敗:", err);
    alert("コピーに失敗しました。お手数ですが手動でコピーしてください");
  }
});

navigator.clipboardが存在するかどうかを最初にチェックし、存在しない場合はユーザーに分かりやすく伝えるようにしました。

この一件以来、「ブラウザの新しいAPIを使うときは、必ずHTTPS前提かどうかを最初に確認する」というのを習慣にしています。

本番環境は基本的にHTTPS化されていることが多いですが、社内の検証環境などでHTTP運用のままになっているケースもあるので、油断は禁物です。

応用・一歩先の使い方

複数のコピーボタンをまとめて扱う

商品一覧など、コピーボタンがページ内に複数ある場合は、イベント委任でまとめて処理すると効率的です。

document.addEventListener("click", async (e) => {
  const btn = e.target.closest(".copy-btn");
  if (!btn) return;

  const targetId = btn.dataset.target;
  const text = document.getElementById(targetId).value;

  try {
    await navigator.clipboard.writeText(text);
    const original = btn.textContent;
    btn.textContent = "コピーしました!";
    setTimeout(() => (btn.textContent = original), 2000);
  } catch (err) {
    console.error("コピーに失敗:", err);
  }
});

data-target属性でコピー元のinputのIDを指定しておけば、ボタンの数が増えても1つのイベントリスナーだけで対応できます。

readonlyのinput以外(テキストや変数)をコピーする

input要素だけでなく、任意の文字列や変数の値をそのままコピーしたい場面もよくあります。

const shareUrl = `${location.origin}/campaign/2026-summer`;

document.getElementById("share-btn").addEventListener("click", async () => {
  await navigator.clipboard.writeText(shareUrl);
});

navigator.clipboard.writeText()は文字列さえ渡せばよいので、inputの値に限らず、JavaScript側で組み立てた文字列をそのままコピー対象にできます。

まとめ

「実践UI実装」シリーズは、ハンバーガーメニューから始まり、モーダル・トースト通知・ドラッグ&ドロップ・無限スクロール、そして今回のコピーボタンまで扱ってきました。

振り返ってみると、どの機能も基本の考え方は共通していて、「HTML/CSSで見た目のパターンをあらかじめ用意しておき、JavaScriptはそこにclassを付けたり外したりして状態を切り替えるだけ」というシンプルな構造でした。

この考え方さえ押さえておけば、今回扱っていない別のUI機能に出会っても、応用して実装できるはずです。

この記事のポイント

  • navigator.clipboard.writeText()でクリップボードへのコピーが簡単に実装できる
  • コピー処理は非同期(Promise)なのでawaitで完了を待ってからUIを更新する
  • Clipboard APIはHTTPS環境(またはlocalhost)でしか使えない点に注意が必要
  • navigator.clipboardの存在チェックとtry/catchで、使えない環境でもエラーで落ちないようにする
  • イベント委任を使えば、複数のコピーボタンを1つのリスナーでまとめて処理できる

次に読むべき記事

次回からは、外部のデータを取得して画面に反映する「fetch API」の基本を扱っていきます。

これまでのUI実装に、実際のサーバーとの通信を組み合わせられるようになる、大事な一歩です。

→ 次の記事:fetch APIの基本:外部データを取得する方法

タイトルとURLをコピーしました