Chrome Extension Clipboard API: Full Guide (2026)

Chrome Extension Clipboard API: Full Guide (2026)

Copying text from a Chrome extension looks like a one-line problem and is not. In Manifest V3 the background context is a service worker with no DOM, navigator.clipboard is undefined there, content scripts inherit the security context of whatever page they land on, and the pages where users most want a copy button are frequently pages where content scripts cannot run at all. The chrome extension clipboard API is really three mechanisms with different constraints, and picking the wrong one produces an extension that works on your test page and fails silently in the wild.

This guide covers all three paths, the permissions that gate them, the rich-format APIs, and a concrete failure checklist. It assumes Manifest V3 throughout, since Manifest V2 is no longer a viable target.

The Core Constraint: Service Workers Have No DOM

Start with why this is hard. In Manifest V2, the background page was a real page with a real document. Copying was three lines: create a textarea, select it, call the legacy copy command. Manifest V3 replaced that background page with a service worker.

A service worker has no document, no window, and no navigator.clipboard. Nothing in the chrome extension clipboard API surface is reachable from a service worker directly. Everything else in this guide is a way of borrowing a document from somewhere else.

Three places to borrow one:

  1. The active tab, by injecting a function with chrome.scripting.executeScript.
  2. An offscreen document, a hidden extension-owned page created with chrome.offscreen.
  3. An extension page that is already open — the popup, the side panel, or the options page.

Each has a distinct set of tradeoffs.

Path 1: Inject Into the Active Tab

The most common entry point into the chrome extension clipboard API. The service worker reads whatever it needs from Chrome APIs, then injects a small function into the current tab to perform the write.

chrome.commands.onCommand.addListener(async (command) => {
  if (command !== 'copy-url') return;

  const [tab] = await chrome.tabs.query({
    active: true,
    currentWindow: true,
  });
  if (!tab?.url) return;

  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: async (text) => {
      await navigator.clipboard.writeText(text);
    },
    args: [tab.url],
  });
});

Why it works: the injected function runs in the page context, where a document exists and is focused.

Permissions required: scripting, plus either activeTab or explicit host permissions. activeTab is the better choice for a keyboard-triggered extension because it grants temporary access to the current tab on user invocation without asking for access to every site.

Where it fails: this is the important part.

  • Restricted URLs. Content scripts cannot be injected into chrome:// pages, the Chrome Web Store, the new tab page in some configurations, other extensions' pages, or the built-in PDF viewer. The executeScript call rejects.
  • Insecure origins. navigator.clipboard is only available in a secure context. A content script inherits the page origin, so on a plain http:// page the clipboard object does not exist. Local development over http://localhost is treated as secure; a staging server on plain HTTP is not.
  • Unfocused documents. If focus is elsewhere — DevTools, another window — the write can reject with a not-allowed error.
  • Sandboxed iframes. A frame without the clipboard-write permissions policy cannot write.

Handle each of those explicitly rather than letting the promise reject into silence.

Path 2: The Offscreen Document

chrome.offscreen (available since Chrome 109) creates a hidden document owned by the extension, specifically so that background logic can reach DOM-only APIs. Clipboard access is one of the documented reasons for creating one.

async function ensureOffscreen() {
  const existing = await chrome.offscreen.hasDocument();
  if (existing) return;

  await chrome.offscreen.createDocument({
    url: 'offscreen.html',
    reasons: ['CLIPBOARD'],
    justification: 'Write the current tab URL to the clipboard',
  });
}

The offscreen page itself is a minimal HTML file containing a textarea and a script that listens for a message from the service worker.

The focus catch. An offscreen document is never focused, because it is never visible. navigator.clipboard.writeText checks document focus and will reject there. The working recipe inside an offscreen document is the legacy path: put the text into a textarea, select it, and call the legacy copy command, which does not have a focus requirement in an extension context with clipboardWrite granted.

function copyViaTextarea(text) {
  const el = document.querySelector('#clipboard-target');
  el.value = text;
  el.select();
  document.execCommand('copy');
}

Yes, that command is deprecated. It is also the mechanism the offscreen clipboard pattern relies on, and Chrome continues to support it precisely because of cases like this one. Treat it as the compatibility layer rather than the primary API.

Why bother with offscreen at all: it works on pages where injection is impossible. If your extension needs to copy something while the user is looking at a chrome:// page or a PDF, this is the only reliable path. It also avoids touching the user's page at all, which is a meaningful privacy property — nothing is injected into any site.

Lifecycle: only one offscreen document can exist per extension at a time. Create it lazily, and close it with chrome.offscreen.closeDocument() when done to avoid holding resources. Some extensions keep it alive for the session to avoid recreation latency on every keystroke; measure before choosing.

Path 3: Copy From an Extension Page

If the copy is triggered from a popup, side panel, or options page, no borrowing is needed. Those pages have a document, they run on the chrome-extension:// origin which is a secure context, and the popup is focused while it is open.

document.querySelector('#copy').addEventListener('click', async () => {
  await navigator.clipboard.writeText(document.querySelector('#value').textContent);
});

This is the cleanest version of the chrome extension clipboard API and should be your default when the interaction starts from your own UI. The catch is that popups close aggressively — if focus moves before the promise resolves, the write can fail. Await the write before doing anything that might dismiss the popup.

Permissions: clipboardWrite and clipboardRead

Two manifest permissions gate the chrome extension clipboard API, and they are not symmetric.

"permissions": ["clipboardWrite", "clipboardRead", "offscreen", "scripting", "activeTab"]

clipboardWrite. Required for the legacy copy command in extension contexts and for writing without a user gesture. It surfaces in the install prompt as modifying clipboard data, which users generally accept without concern. Declare it if you use the offscreen or textarea path.

clipboardRead. Required to read clipboard contents from an extension context, including the legacy paste command and navigator.clipboard.readText without a per-use prompt. This is a materially stronger ask — the clipboard routinely contains passwords, tokens, and personal data. Store review treats read access with more scrutiny, and users notice it in the permission prompt. Do not request clipboardRead if your extension only copies. A surprising number of extensions request both out of copy-paste manifest habit.

activeTab versus host permissions. For a keyboard-triggered copy extension, activeTab is enough and is dramatically less invasive than requesting access to all sites. It grants temporary access to the current tab when the user invokes the extension, which is exactly the trigger model a copy shortcut has.

The minimal-permission version of a URL copier is a good illustration of the principle: clipboard access, nothing else, no host permissions, no network. That is the entire permission surface of Ctrl+Shift+C, which copies the current tab URL on a keystroke and makes no network calls at all. Small permission surfaces are easier to review, easier to trust, and easier to keep working across Chrome releases.

Writing Rich Content With ClipboardItem

Plain text covers most cases. When you need formatting, ClipboardItem lets you write several representations at once and lets the pasting application choose.

async function copyLink(url, title) {
  const html = `<a href="${url}">${title}</a>`;
  const item = new ClipboardItem({
    'text/plain': new Blob([url], { type: 'text/plain' }),
    'text/html': new Blob([html], { type: 'text/html' }),
  });
  await navigator.clipboard.write([item]);
}

Paste that into a document editor and you get a clickable link with the page title. Paste it into a code editor or terminal and you get the bare URL. One copy action, correct behavior in both destinations.

Points worth knowing:

  • Format support is limited. Chrome supports text/plain, text/html, and image/png for writes. Arbitrary MIME types are rejected unless you use the web custom format mechanism, where a type prefixed with web carries unsanitized data between applications that opt into it.
  • HTML is sanitized. Scripts and event handlers are stripped on write. Do not rely on the clipboard as a transport for executable markup.
  • Escape your interpolations. The example above is illustrative; a title containing markup characters needs escaping before it goes into an HTML string.
  • Blobs can be promises. Chrome accepts a promise resolving to a Blob as a ClipboardItem value, which lets you start the write inside a user gesture while the data is still being produced.
  • Images. Writing a PNG blob works. Reading images back requires navigator.clipboard.read and the read permission.

Reading the Clipboard

If you genuinely need to read — a paste-and-transform extension, for instance — the API is:

const text = await navigator.clipboard.readText();

Constraints:

  • Extension pages with clipboardRead can read without a per-use prompt.
  • Content scripts follow web page rules, which means the clipboard-read permission prompt applies on the page origin and a user gesture is required. This is often surprising: the extension permission does not extend to the page context in the way developers expect.
  • navigator.clipboard.read returns ClipboardItem objects and is the route to image and HTML data. Iterate the types and pick what you support.
  • Treat what you read as untrusted input. Clipboard content comes from anywhere on the system. Never inject it into the DOM as HTML without sanitizing.

Keyboard Shortcuts as the Trigger

Most clipboard extensions are shortcut-driven, which means wiring chrome.commands to the copy path.

"commands": {
  "copy-url": {
    "suggested_key": {
      "default": "Ctrl+Shift+C",
      "mac": "Command+Shift+C"
    },
    "description": "Copy the current tab URL"
  }
}

Two things routinely bite here:

Suggested keys are suggestions. Chrome does not assign a shortcut that conflicts with a built-in binding or another extension. It silently leaves the command unbound, and the user assumes the extension is broken. Detect an unbound command with chrome.commands.getAll() and surface a message pointing at the shortcuts settings page.

Some combinations are reserved. Chrome reserves a set of bindings that extensions cannot claim. Design for rebinding rather than assuming your default sticks. The user-facing side of that is covered in how to set custom shortcuts chrome extension.

Feedback: Confirming the Copy Happened

A clipboard write is invisible. Without feedback, users press the shortcut twice, then check by pasting, which erases the entire time saving.

Options, roughly in order of intrusiveness:

  • Badge text. chrome.action.setBadgeText with a short confirmation, cleared after a second or two. No page access required, works everywhere including restricted pages.
  • An injected toast. A small element added to the page by the content script and removed after a moment. Nicest looking, but requires page access and does nothing on restricted pages.
  • A notification. chrome.notifications is heavier than the action deserves and users find it noisy.

Badge feedback is the pragmatic default because it is the only one that works in every context where the copy itself works.

The Failure Checklist

Before publishing anything that uses the chrome extension clipboard API, test all of these:

  1. HTTPS page — the happy path.
  2. Plain HTTP pagenavigator.clipboard is undefined in the content script. Does your code fall back or throw?
  3. A chrome:// page — injection fails. Does the offscreen path take over?
  4. The Chrome Web Store — injection is blocked by policy, and this is a page users test on constantly.
  5. The built-in PDF viewer — a special case that behaves unlike normal pages.
  6. A page inside an iframe-heavy site — check you are targeting the top frame.
  7. With DevTools focused — document focus checks fail here, which is exactly when developers test.
  8. In incognito — behavior depends on whether your extension runs in split or spanning mode.
  9. Immediately after browser startup — the service worker may be cold. Does the first invocation work, or does it lose a race against initialization?
  10. With a long value — very large clipboard payloads behave differently across platforms.

Items 2, 4, and 7 account for the majority of one-star reviews on copy extensions. They are all easy to fix and all invisible if you only test on your own HTTPS demo page.

Manifest V2 to V3: What Actually Changed

For anyone porting an older extension, the differences in the chrome extension clipboard API are concentrated in one place.

Manifest V2: background page with a full DOM. Create a textarea, select, execute the copy command. Worked from the background context on any page, no injection needed.

Manifest V3: no background DOM. Choose injection, offscreen, or an extension page. Legacy copy still exists but has to run inside a document you own or borrow. The offscreen API exists specifically to fill this gap.

The net effect is more code for the same result, in exchange for a background context that does not stay resident. If you are writing an extension from scratch rather than porting, the structure in how to build a chrome extension walks through the surrounding scaffolding, and copy url to clipboard extension covers what the finished behavior should feel like from the user side.

Frequently Asked Questions

Can a Manifest V3 service worker write to the clipboard directly? No. Service workers have no DOM and no navigator.clipboard, so the write must happen inside a document — an injected content script, an offscreen document, or one of your own extension pages.

What is the offscreen document API used for? It creates a hidden, extension-owned DOM document so background logic can use DOM-only APIs. Clipboard access is one of the supported reasons, alongside audio playback and DOM parsing.

Do I need the clipboardWrite permission? Declare it if you use the legacy copy command or write from an extension context without a user gesture. A content script using navigator.clipboard on a secure, focused page can work without it, but declaring it removes an entire class of edge-case failures.

Why does navigator.clipboard.writeText fail with a not allowed error? Almost always because the document is not focused or the page is not a secure context. Content scripts inherit the page origin, so a plain http page has no clipboard API to call at all.

How do I copy rich text and plain text at the same time? Build one ClipboardItem containing both a text/plain blob and a text/html blob and pass it to navigator.clipboard.write. The receiving application selects whichever representation it understands.

Does clipboard read require a separate permission? Yes, clipboardRead, and it is a much stronger ask than write because clipboards routinely contain credentials. Request it only if reading is core to the extension.

Where does clipboard copying fail even with correct permissions? On pages where content scripts cannot be injected — chrome:// URLs, the Chrome Web Store, other extension pages, and the built-in PDF viewer. An offscreen document is the reliable path in those contexts.

Build It Small, Test It Everywhere

The chrome extension clipboard API rewards a narrow design: pick the smallest permission set that works, choose injection or offscreen deliberately rather than by accident, give the user visible confirmation, and test the ten failure cases before shipping rather than after the reviews arrive. If you want to see the pattern in a finished, deliberately minimal form, Ctrl+Shift+C does one thing — copies the current tab URL on a keystroke — with clipboard permission only, no network calls, and no data collection. Install it to see the target behavior, then go build yours.

Try Ctrl+Shift+C

Copy any URL with one keyboard shortcut. Free forever, no data collected.