> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Offscreen documents in Manifest V3

> Create, reuse, and close chrome.offscreen documents from a service worker. Bundle the offscreen page with the pages/ folder and message it reliably.

Manifest V3 service workers have no DOM. When background logic needs DOM parsing, audio playback, clipboard access, or another window-only API, Chrome's answer is an offscreen document: an invisible extension page you create on demand with `chrome.offscreen`.

## Setup

Request the permission in `manifest.json`:

```json manifest.json theme={null}
{
  "permissions": ["offscreen"]
}
```

The offscreen page is not a manifest entrypoint, so declare it through the [`pages/` special folder](/docs/features/special-folders) and Extension.js compiles it like any other HTML entry:

```plaintext theme={null}
pages/
└── offscreen.html
```

```html pages/offscreen.html theme={null}
<!doctype html>
<html>
  <body>
    <script src="./offscreen.ts" type="module"></script>
  </body>
</html>
```

## Create, reuse, close

Chrome allows one offscreen document per extension, and calling `createDocument` while one exists throws. The reliable pattern is an ensure function that checks for an existing document first:

```ts background.ts theme={null}
let creating: Promise<void> | null = null;

async function ensureOffscreen() {
  const contexts = await chrome.runtime.getContexts({
    contextTypes: ["OFFSCREEN_DOCUMENT" as chrome.runtime.ContextType],
  });
  if (contexts.length > 0) return;

  if (!creating) {
    creating = chrome.offscreen.createDocument({
      url: "pages/offscreen.html",
      reasons: [chrome.offscreen.Reason.DOM_PARSER],
      justification: "Parse HTML strings that the service worker cannot",
    });
  }
  await creating;
  creating = null;
}
```

The `creating` lock matters: two events can race into `ensureOffscreen` before the document exists, and the second `createDocument` call would throw.

Close it when the work is done to free memory:

```ts theme={null}
await chrome.offscreen.closeDocument();
```

## Choosing a reason

The `reasons` array tells Chrome why the document exists. Common values:

| Reason | Use case |
| - | - |
| `DOM_PARSER` | Parse or sanitize HTML strings |
| `AUDIO_PLAYBACK` | Play sound from the background |
| `CLIPBOARD` | Read or write the clipboard |
| `DOM_SCRAPING` | Extract data from rendered markup |
| `BLOBS` | Create and manage blob URLs |
| `USER_MEDIA` | Access getUserMedia for recording |

One behavior worth knowing: with `AUDIO_PLAYBACK`, Chrome closes the document automatically about 30 seconds after audio stops playing. For other reasons the document lives until you close it or the extension unloads.

## Clipboard access from the background

Writing to the clipboard is the most common reason to reach for an offscreen
document. `navigator.clipboard` lives on `window`, and a manifest v3 service
worker has no `window`, so the write has to happen on a real page.

Ask for the permission and the reason:

```json manifest.json theme={null}
{
  "permissions": ["offscreen", "clipboardWrite"]
}
```

Build the offscreen page with a textarea that the copy command can use:

```html pages/offscreen.html theme={null}
<!doctype html>
<html>
  <body>
    <textarea id="clipboard-buffer"></textarea>
    <script src="./offscreen.ts" type="module"></script>
  </body>
</html>
```

```ts pages/offscreen.ts theme={null}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.target !== "offscreen" || message.type !== "copy") return;

  const buffer = document.getElementById("clipboard-buffer") as HTMLTextAreaElement;
  buffer.value = message.text;
  buffer.select();
  document.execCommand("copy");
  sendResponse({ ok: true });
});
```

Then send the text from the service worker:

```ts background.ts theme={null}
async function copyToClipboard(text: string) {
  await ensureOffscreen();
  await chrome.runtime.sendMessage({ target: "offscreen", type: "copy", text });
}
```

Pass `chrome.offscreen.Reason.CLIPBOARD` to `createDocument` for this case.

`document.execCommand("copy")` is the call that works here. The newer
`navigator.clipboard.writeText` needs a focused document, and an offscreen
document is never focused.

Reading the clipboard needs the `clipboardRead` permission and the same
document.

A content script or an extension page that the user opened does not need any of
this. Both have a real `window`, so `navigator.clipboard.writeText` works after
a user gesture.

### Extension.js ships the page for you

`chrome.offscreen.createDocument` takes a URL string, not an import. Extension.js
reads that string during the build and adds the HTML page, plus everything that
the page loads, to the output. A page that only the service worker names still
reaches `dist/`.

The string has to be a plain literal for that to work. A URL that you build at
runtime is invisible to the build, and the page is left out.

## Talking to the document

Offscreen documents use standard runtime messaging. Scope your messages so other surfaces ignore them:

```ts pages/offscreen.ts theme={null}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message.target !== "offscreen") return;
  const doc = new DOMParser().parseFromString(message.html, "text/html");
  sendResponse({ title: doc.title });
  return true;
});
```

## Firefox

Firefox does not implement `chrome.offscreen`. Its Manifest V3 background runs as an event page that already has DOM access, so the same DOM work runs directly in the background script there. Use [browser-specific manifest fields](/docs/features/browser-specific-fields) and a capability check (`typeof chrome.offscreen !== "undefined"`) to branch.

## See also

* [Background scripts](/docs/implementation-guide/background)
* [Special folders](/docs/features/special-folders)
* [Messaging](/docs/implementation-guide/messaging)
* [Lazy loading heavy libraries](/docs/implementation-guide/lazy-loading)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.