コードブロック用のコピーボタンを作成する
コードスニペットを手動で選択してコピーしようと苦戦している人を見たことがあるか、あるいは自分で経験したことがあるなら、コードブロック用のコピーボタンが重要な理由はすでにお分かりでしょう。これは小さなUIの工夫ですが、特にキーボード、音声コマンド、タッチスクリーンで操作するユーザーにとっては大きな違いを生みます。
本記事では、ライブラリを一切使わず、モダンな Clipboard API を使って、シンプルで信頼性の高いコードコピーUIを構築する方法を解説します。
重要なポイント
navigator.clipboard.writeText()を使用すると、Promise ベースのモダンなコピー処理を実装できます。ただし、セキュアコンテキスト(HTTPS または localhost)とユーザー起点のイベントが必要です。- コードを取得するには
innerHTMLではなくtextContentを使用してください。シンタックスハイライターの<span>ラッパーが実際のコードと一緒にコピーされるのを防げます。 - 呼び出しを
try/catchブロックで囲み、権限エラーを処理してユーザーに明確な視覚的フィードバックを提供しましょう。 - ボタンに
aria-labelを追加してアクセシビリティを向上させます。特にテキストラベルの代わりにアイコンを使う場合に重要です。 - 非推奨の
document.execCommand('copy')は、レガシー環境向けの最終手段としてのみ使用すべきです。
Clipboard API の writeText メソッドの仕組み
JavaScript でクリップボードにコピーするモダンな方法は navigator.clipboard.writeText() です。Promise ベースで非同期、現行のすべてのブラウザでサポートされています。
使う前に押さえておくべき点が2つあります:
- セキュアコンテキストが必須です。 Clipboard API は HTTPS でのみ動作します。
localhostであれば、平文 HTTP でもセキュアとして扱われるため、開発時には問題ありません。 - ユーザー操作によってトリガーされる必要があります。
clickイベントハンドラ内で呼び出せば、この要件は自動的に満たされます。
await navigator.clipboard.writeText("your text here");
これがこの機能の核心部分です。
コードブロックからテキストを抽出する
HTML はおそらく次のような形になっているはずです:
<pre><code>const greeting = "hello";</code></pre>
生のテキストを取得するには、innerHTML ではなく textContent を使います。innerHTML を使うと、コードと一緒に HTML タグもコピーされてしまい、これは決して望ましい結果ではありません。
const code = document.querySelector("pre code").textContent;
コードブロックで Prism.js や Highlight.js のようなシンタックスハイライターを使用している場合、ハイライターはトークンを <span> 要素でラップします。textContent はそれらをすべて取り除いてプレーンテキストのみを返すので、ユーザーのクリップボードに入るべき内容そのものになります。
Discover how at OpenReplay.com.
コードブロック用コピーボタンを構築する
以下が完全に動作する実装例です:
document.querySelectorAll("pre").forEach((block) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = "Copy";
button.setAttribute("aria-label", "Copy code to clipboard");
button.addEventListener("click", async () => {
const code = block.querySelector("code")?.textContent ?? "";
try {
await navigator.clipboard.writeText(code);
button.textContent = "Copied!";
setTimeout(() => (button.textContent = "Copy"), 2000);
} catch (err) {
console.error("Copy failed:", err);
button.textContent = "Failed";
setTimeout(() => (button.textContent = "Copy"), 2000);
}
});
block.style.position = "relative";
block.appendChild(button);
});
ここで注目すべきポイントがいくつかあります:
aria-labelは、スクリーンリーダー向けにボタンへアクセシブルな名前を付与します。後でテキストをアイコンに置き換える場合は特に重要です。e.currentTargetは、ボタンが SVG アイコンなどの子要素を含む場合、e.targetよりも安全です。e.targetはボタンではなくアイコン自体を指す可能性があるためです。(必要に応じて、クリックハンドラにeventパラメータを追加してアクセスできます。)try/catchブロック は、権限拒否や予期しない失敗を適切に処理し、サイレントエラーではなくユーザーに見える形でフィードバックを返します。
古いブラウザへの対応は?
navigator.clipboard はモダンブラウザで非常に広くサポートされています。古い環境をサポートする必要がある場合、フォールバックとして document.execCommand('copy') が存在しますが、これは非推奨で信頼性に欠けます。機能チェックを行ったうえで、最終手段としてのみ使用してください:
if (!navigator.clipboard) {
// document.execCommand('copy') を使ったレガシーフォールバック
}
現在構築されている多くのドキュメントサイトや開発者向けツールでは、Clipboard API のみに頼っても問題ありません。
まとめ
実用的なコードブロック用コピーボタンは、3つのポイントに集約されます。textContent でテキストを取得し、navigator.clipboard.writeText() で書き込み、成功または失敗時にユーザーへ明確なフィードバックを返すことです。aria-label でボタンのアクセシビリティを保ち、エラーを明示的に処理すれば、30 行未満の素の JavaScript で本番投入可能な実装が完成します。
FAQ
Clipboard API はセキュアコンテキストを要求するため、本番環境では HTTPS が必要です。ただし、ブラウザはデフォルトで localhost をセキュアとして扱うため、開発中は平文 HTTP でも問題なく動作します。192.168.x.x のようなローカルネットワーク IP でテストしている場合、それはセキュアとみなされないため API は失敗します。代わりに localhost を使うか、HTTPS 対応の開発サーバーを構築してください。
いいえ。Clipboard API はユーザーアクティベーションを必要とします。つまり、click、keydown、pointerup などのユーザーが起動したイベントハンドラ内で呼び出す必要があります。ページ読み込み時や setTimeout 内で writeText を呼び出すと、サイレントに失敗するか権限エラーがスローされます。この制限により、悪意のあるサイトが同意なしにクリップボードを乗っ取ることを防いでいます。
textContent プロパティは、HTML に書かれているとおりに改行やインデントを含む空白をすでに保持しています。pre 要素と code 要素に適切にフォーマットされたソースコードが含まれていれば、コピーされるテキストは元の内容と一致します。innerText の使用は避けてください。CSS のレンダリングに基づいて空白が正規化される可能性があり、ブラウザ間で結果が一貫しなくなります。
e.target は実際にクリックを受け取った要素を指します。これはボタン内の SVG アイコンのような子要素である可能性があります。一方 e.currentTarget は、イベントリスナーがアタッチされた要素 — この場合はボタン自体 — を常に指します。currentTarget を使用することで、ユーザーがボタン内のネストされたアイコンや span をクリックした際に発生するバグを防げます。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.