How to Capture Website Screenshots Across Browsers with a Chrome Extension
Build a user-triggered browser extension that captures the visible tab, understand permission differences, and choose the right path for full-page screenshots.
A Chrome extension can capture the visible viewport of the active tab with chrome.tabs.captureVisibleTab(). It does not automatically capture the entire scrollable page. In Chrome, declare either activeTab or <all_urls>; for a user-triggered capture, activeTab keeps access scoped to the tab the user invokes the extension on. Chrome documents a limit of two capture calls per second. Chrome Tabs API reference
Firefox provides the corresponding browser.tabs.captureVisibleTab() method, with a permission difference for Firefox 125 and earlier. This article builds a simple Chrome extension and explains what must be adapted for Firefox. The cited API guidance covers Chrome and Firefox; it does not establish identical behavior for Edge or a reliable shared full-page implementation across all three browsers.
1. Decide what kind of screenshot you need
| Need | Practical approach | Important limit |
|---|---|---|
| What the user currently sees | Call the active browser’s visible-tab capture API. | Captures the visible area, not the whole document. |
| A whole page in Firefox | Use Firefox’s built-in screenshot feature, which can save visible or full-page captures. | This is a browser feature; it does not make the extension API cross-browser or guarantee identical results on complex pages. Mozilla Support |
| A whole page in a custom extension | Design and validate a browser-specific scrolling/capture/stitching approach or use a browser facility that supports the needed output. | The visible-tab APIs described here are not full-page methods. Nested scrollers, sticky UI, lazy loading, and very tall pages need targeted testing. |
If you are asking “How do I take a screenshot of an entire web page?”, first distinguish a whole-document image from a screenshot of the current viewport. A viewport capture is a small, well-defined extension task. A robust full-page capture is a separate engineering problem.
2. Build a minimal Chrome extension
This Manifest V3 example captures the current visible tab when the user clicks the extension action, then opens the PNG data URL in a new tab. The workflow follows Google’s official sample. Chrome tab screenshot sample
Files
visible-shot/
manifest.json
service-worker.js
manifest.json
{
"manifest_version": 3,
"name": "Visible Tab Screenshot",
"version": "1.0.0",
"description": "Capture the visible area of the active tab.",
"permissions": ["activeTab"],
"background": {
"service_worker": "service-worker.js"
},
"action": {
"default_title": "Capture visible tab"
}
}
service-worker.js
chrome.action.onClicked.addListener(async (tab) => {
try {
// The action click is the user invocation that activates activeTab.
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
format: "png"
});
await chrome.tabs.create({ url: dataUrl });
} catch (error) {
console.error("Could not capture the visible tab:", error);
}
});
To try it locally, open Chrome’s extensions page, enable Developer mode, choose Load unpacked, and select the visible-shot directory. Open a normal website and click the extension action. The result is an image opened in a tab; this minimal example does not add a download button, clipboard flow, popup, or full-page stitching.
Choose an output format
The API accepts image details such as a format and, for JPEG, a quality value. PNG is suitable when you want lossless output. JPEG can reduce file size for photographic content, with quality trading size against fidelity. Check the current API reference for the supported options and behavior in the target browser. The example requests PNG and avoids assuming that every browser offers the same format options.
3. Permissions and privacy
For Chrome’s captureVisibleTab(), the documented permission choices are activeTab or <all_urls>. activeTab grants temporary access to the current tab following a user invocation and avoids asking for persistent access to every site. Use broad host access only when the extension actually needs it, and explain the scope plainly to users. The separate tabs permission is not a substitute for these capture permissions.
The extension above processes the returned image locally by opening its data URL. If you later upload captures, add clear disclosure, secure transport, appropriate retention, and a user action that makes the upload expected. Screenshots can contain account details, private messages, or other sensitive page content.
4. Firefox and Edge behavior
Firefox
Firefox’s WebExtensions API uses browser.tabs.captureVisibleTab() and returns a data URL. Its documentation notes that Firefox 125 and earlier required <all_urls> for this method; consult the current MDN reference when setting your supported-version range. Do not assume that declaring activeTab alone works identically in every older Firefox release.
A portable extension can keep the capture logic behind a small adapter, but it still needs browser-specific testing and packaging. In Firefox code, the call is commonly expressed as browser.tabs.captureVisibleTab(windowId, options); Chrome’s namespace is chrome.tabs. Promise and manifest compatibility details also depend on the browser and version. A shared name or polyfill does not prove identical permissions or output behavior.
Edge
This guide does not claim Edge compatibility based on Chrome’s API documentation. Before shipping, check the current Microsoft Edge extension documentation, supported manifest version, permission behavior, and store requirements, then verify the capture on the Edge versions you support.
5. Visible viewport versus full-page capture
captureVisibleTab() captures the visible portion of the active tab. It does not scroll the document, trigger lazy-loaded sections, or stitch multiple images into a full-page result. A full-page implementation usually has to coordinate page scrolling and multiple captures or use a browser-specific facility. That introduces seams, duplicated or missing sticky headers, changing page content, and memory pressure from large images.
Before building a full-page workflow, define its boundaries and test representative sites. Include nested scroll containers, sticky and fixed elements, lazy-loaded images, pages that change while scrolling, very tall documents, cross-origin frames, and restricted browser pages. Do not promise these cases work merely because a viewport capture succeeds.
6. Reliability, performance, and output handling
- Respect the rate limit. Chrome documents a maximum of two
captureVisibleTabcalls per second and describes the operation as expensive. Capture on an intentional user action, debounce repeated clicks, and queue work instead of firing rapid calls. Chrome API limit - Handle failures. Keep the call in
try/catch; surface a useful message in a production popup or notification instead of silently failing. - Keep image data bounded. A data URL is convenient for a simple demo, but it can be large. For a download flow, convert it to a
Bloband use an object URL, then revoke that object URL when it is no longer needed. - Capture at the right moment. The API captures what is visible at invocation. If a page is still rendering, wait for the user or implement a deliberate readiness strategy; do not assume the screenshot waits for network idle or animations.
- Test window focus and tab selection. The method captures the currently active tab in the specified window, so switching tabs or windows during a workflow can change what is captured.
- Do not use capture as a hidden recorder. Make the action and any storage or transmission visible to the user.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Permission error or rejected promise | Neither required Chrome permission is present, or the expected user invocation did not grant temporary access. | Declare activeTab and invoke capture from the extension action, or use <all_urls> only if broad access is necessary. Check the browser console and current API documentation. |
| Only the viewport appears | The method captures visible tab content, not the full document. | Use a genuine full-page feature or implement and validate a separate scrolling/capture strategy. |
| Firefox works on newer versions but fails on older ones | Firefox 125 and earlier had a documented <all_urls> requirement. |
Set a minimum supported version or declare the permission required for the versions you support, and explain it to users. |
| Repeated captures fail or behave inconsistently | Calls may exceed Chrome’s two-per-second limit or consume too many resources. | Debounce, queue, and keep captures user-driven. |
| Some areas are blank or incomplete | Dynamic rendering, lazy loading, protected pages, or browser-specific restrictions may affect the result. | Wait for the expected page state, test the specific page type, and do not assume an extension can capture every browser surface. |
| Large screenshot is slow to display or save | Data URLs and very large image dimensions use memory and take time to process. | Avoid unnecessary repeated captures; use a blob/object URL for download flows and release it after use. |
8. Or skip the browser setup
If you need a website screenshot from code or an AI agent rather than a screenshot of the user’s current tab, ScreenshotNeo provides a screenshot API and MCP server. A single GET request accepts a URL and returns an image or PDF. See the ScreenshotNeo API documentation.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image:
image.write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. These are URL-based captures, so use the extension method when you need the actual visible state of a user’s active tab.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
9. Frequently asked questions
Does captureVisibleTab take a screenshot of the entire web page?
No. It captures the visible tab area. Whole-page capture requires a different feature or an additional implementation.
Do I need the tabs permission?
For Chrome’s capture method, the documented requirement is activeTab or <all_urls>. The tabs permission serves other tab-information use cases and does not replace the capture permission.
Can one extension use the same code in Chrome, Firefox, and Edge?
Do not assume so. Chrome and Firefox have corresponding APIs, but permission and compatibility details differ; this research does not verify Edge behavior. Validate each target browser and version.
Can the extension take screenshots automatically every second?
Chrome documents a maximum of two calls per second, and recommends against frequent calls because capture is expensive. Keep capture deliberate and rate-limited.
How can I capture a full page in Firefox without writing an extension?
Firefox’s built-in screenshot feature supports visible or full-page captures that can be copied or saved. See Mozilla’s instructions.


