JavaScript Screen Capture API and when to use a screenshot API

Use the JavaScript Screen Capture API to save a screen as a PNG. Learn its permission requirements, limitations, and when to use a website screenshot API.

Blog post8 min read

Written by

Dmytro Krasun

Published on

The JavaScript Screen Capture API lets a webpage ask for a live video stream of a screen, window, or browser tab, chosen by the user. Its main method is getDisplayMedia(). To get a screenshot, you grab one frame from that stream and save it as an image.

People often reach for it when what they really want is “turn this URL into an image.” It can’t do that. There’s no url parameter. It only captures what’s already on the user’s screen, and only after they agree.

So there are two separate problems:

  • “Let a user show me what they’re seeing.” For example, a bug report with a screenshot attached. The Screen Capture API is a good fit, and the code is below.
  • “Render this website as an image.” For example, a thumbnail for every URL submitted to your directory. For that you need a browser you control: Playwright, Puppeteer, or a screenshot API. Jump to that part.

How getDisplayMedia works

Calling getDisplayMedia() opens the browser’s “choose what to share” dialog. Once the user picks something and approves, you get a MediaStream with a video track. You can show it in a <video> element, record it, or send it over WebRTC. Which sources are offered depends on the browser and the OS. See MDN’s guide for the details.

Save a screen capture as a PNG

The plan:

  1. Call getDisplayMedia() from a button click.
  2. Attach the stream to a <video> element.
  3. Wait for the first frame. Having a stream doesn’t mean there are pixels yet, and skipping this step is the most common reason for blank screenshots. The loadeddata event tells you a frame is ready.
  4. Draw that frame on a canvas and export a PNG.
  5. Stop the stream so the browser’s “sharing” indicator goes away.

Save this as capture.html and serve it over HTTPS or from localhost. The API requires a secure context. A file:// URL can also qualify, but browser support and permission rules vary; the example checks both the context and the API before enabling capture.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Capture a screenshot</title>
</head>
<body>
<button id="capture" type="button">Capture screenshot</button>
<p id="status" role="status"></p>
<img id="preview" alt="Captured screenshot" hidden
style="max-width: 100%" />
<a id="download" download="screen-capture.png" hidden>Download PNG</a>
<script type="module">
const button = document.querySelector("#capture");
const status = document.querySelector("#status");
const preview = document.querySelector("#preview");
const download = document.querySelector("#download");
let imageURL;
if (!window.isSecureContext ||
!navigator.mediaDevices?.getDisplayMedia) {
button.disabled = true;
status.textContent =
"Screen capture needs a supported browser and a secure context. Try HTTPS or localhost.";
}
button.addEventListener("click", async () => {
button.disabled = true;
status.textContent = "Choose what to share.";
let stream;
let frameTimer;
const video = document.createElement("video");
video.muted = true;
video.playsInline = true;
try {
// Call this first in the click handler. An await before it can
// use up the user gesture, and the browser will refuse.
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false,
});
const frameReady = new Promise((resolve, reject) => {
video.addEventListener("loadeddata", resolve, { once: true });
video.addEventListener("error", () => {
reject(new Error("The captured video couldn't be loaded."));
}, { once: true });
frameTimer = setTimeout(() => {
reject(new Error("No frame arrived. Try again."));
}, 10000);
});
video.srcObject = stream;
await Promise.all([frameReady, video.play()]);
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0);
const blob = await new Promise((resolve) => {
canvas.toBlob(resolve, "image/png");
});
if (!blob) throw new Error("Couldn't create the PNG.");
if (imageURL) URL.revokeObjectURL(imageURL);
imageURL = URL.createObjectURL(blob);
preview.src = imageURL;
preview.hidden = false;
download.href = imageURL;
download.hidden = false;
status.textContent = "Done. Sharing has stopped.";
} catch (error) {
status.textContent = error.name === "NotAllowedError"
? "Sharing was cancelled or blocked."
: `Capture failed: ${error.message}`;
} finally {
clearTimeout(frameTimer);
stream?.getTracks().forEach((track) => track.stop());
video.srcObject = null;
button.disabled = false;
}
});
</script>
</body>
</html>

Click Capture screenshot, pick a tab, window, or screen, and approve. The page grabs one frame, stops sharing, and shows a preview with a download link. Nothing leaves the browser.

Two details are easy to get wrong:

  • The canvas is sized from videoWidth and videoHeight, the real resolution of the captured frame, not the CSS size of any preview. Otherwise you get a blurry or cropped image.
  • The capture shows the screen at the moment the frame arrives, which is after the user clicked through the dialog. If a tooltip or hover state was what they wanted to show, it’s probably gone by then.

If you use this for bug reports, show the preview and let the user decide whether to send it. Screenshots catch things people didn’t mean to share: other tabs, notifications, open documents.

Permissions and browser support

The rules come from the W3C spec, and browsers enforce them strictly:

  • A secure context is required. Use HTTPS, or localhost during development, and keep the feature check in the example.
  • It must start from a user action, like a click. Call getDisplayMedia() first thing in the handler. If you await a network request before it, the browser may decide the click is too old and reject the call.
  • The user always picks the source. Options like displaySurface are hints. You can’t force a particular tab, and the permission isn’t remembered for next time.
  • Within one session, you can grab as many frames as you like without asking again. The example stops right after the first frame, but a “take another” flow could keep the stream open.

Feature detection tells you the API exists, not that the user or the OS will allow it. Check browser compatibility for the devices you care about. Mobile support in particular is limited.

Errors you’ll actually see

A user clicking Cancel is normal, so treat it as a non-event in your UI. The others usually point to a setup problem:

ErrorUsually means
NotAllowedErrorThe user cancelled, or a permissions policy blocks capture
InvalidStateErrorNo user gesture, or the page wasn’t active and focused
NotReadableErrorThe OS or hardware wouldn’t let the browser read the source
TypeErrorInvalid options, like video: false, or min/exact constraints

More detail is in MDN’s exceptions list.

If the capture button lives in an iframe, the parent page has to allow it through the display-capture permissions policy. The user still sees the sharing dialog either way.

Recording instead of a single image

To record video, pass the stream to MediaRecorder, collect chunks from dataavailable, and combine them when recording stops. Give the user a visible stop button, and stop the tracks when you’re done. If you want a specific format, check it first with MediaRecorder.isTypeSupported().

Audio is unreliable. audio: true is a request, not a promise. Whether you get an audio track depends on the browser, the OS, and what the user shared (tab audio works more often than full-screen audio). Check stream.getAudioTracks() before telling anyone the recording has sound. See MDN on capturing audio.

Picking the right tool

Here’s how I’d choose:

What you’re buildingStart with
“Attach a screenshot” in a bug reportScreen Capture API, with a preview
Screen sharing or recording a demoScreen Capture API with WebRTC or MediaRecorder
Export a card or chart from your own pageA DOM-to-image library like html2canvas
Screenshots in tests, or behind a scripted loginPlaywright or Puppeteer
Previews for user-submitted URLs, scheduled capturesA screenshot API

html2canvas

If you just want to export part of your own page, such as a receipt, a chart, or a share card, a library like html2canvas might be enough. It doesn’t take a real screenshot. It reads the DOM and redraws it on a canvas, so some CSS isn’t supported and images from other domains can be missing. Try it on your real content before relying on it.

Playwright and Puppeteer

These give your script a real browser that loads URLs, clicks, types, and takes screenshots. Playwright’s screenshot API supports fullPage: true, which getDisplayMedia() has no equivalent for. They’re a good choice when you need to click through the app before capturing, or when you already have a browser test suite.

The cost is running and maintaining the browsers yourself. For local examples, see our JavaScript and TypeScript screenshot guide.

When to use a screenshot API

A website screenshot API fits when you have a URL, need an image, and no human is around to click “Share.” It’s also the right call when running headless browsers in production would become a project of its own: memory, crashes, fonts, cookie banners, and so on.

Typical uses:

  • Thumbnails for a directory of websites
  • Scheduled captures for change monitoring
  • Webpage images in reports
  • Full-page captures at a consistent viewport
  • Refreshing a preview whenever a user submits a URL

ScreenshotOne is the one we build. You send a URL and options such as viewport size, full page, a selector, or cookie banner blocking, and get back an image.

The trade-off is the reverse of getDisplayMedia(): the API opens the page in a fresh browser, so it doesn’t see the user’s login, open menus, or half-filled forms. If you need exactly what the user is looking at, use screen capture. For private pages rendered on the server, set up authentication.

Example: capture a URL from Node.js

Save as screenshot.mjs, put your API key in SCREENSHOTONE_ACCESS_KEY, and run node screenshot.mjs (Node.js 22+):

import { writeFile } from "node:fs/promises";
const accessKey = process.env.SCREENSHOTONE_ACCESS_KEY;
if (!accessKey) {
throw new Error("Set SCREENSHOTONE_ACCESS_KEY first.");
}
const endpoint = new URL("https://api.screenshotone.com/take");
endpoint.search = new URLSearchParams({
url: "https://example.com/",
format: "png",
full_page: "true",
viewport_width: "1280",
viewport_height: "800",
block_cookie_banners: "true",
}).toString();
const response = await fetch(endpoint, {
headers: { "X-Access-Key": accessKey },
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) {
throw new Error(`Screenshot request failed: HTTP ${response.status}`);
}
const image = Buffer.from(await response.arrayBuffer());
const pngHeader = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
if (!image.subarray(0, 8).equals(pngHeader)) {
throw new Error("The response wasn't a PNG image.");
}
await writeFile("website.png", image);
console.log("Saved website.png");

Run this on your server, never in the browser, so the key stays secret.

For pages that load data after the first render, add wait_for_selector with an element that appears once the content is ready. It waits for the element to exist, not for its images to finish, so look at a few results and adjust if needed.

If you need repeatable URL captures, start with the ScreenshotOne getting started guide. If you need what’s on the user’s screen, the HTML example above is a good place to start.

Frequently Asked Questions

If you read the article, but still have questions. Please, check the most frequently asked. And if you still have questions, feel free reach out at support@screenshotone.com.

Can JavaScript capture a screen without permission?

No. A webpage using getDisplayMedia can't silently capture the screen. The user has to pick what to share and approve it every time a new capture session starts.

Can the Screen Capture API take a full-page screenshot?

No. getDisplayMedia captures the screen, window, or tab the user selects, exactly as it appears. To get one image of a long, scrolling webpage, use browser automation or a website screenshot API.

Can I use getDisplayMedia in Node.js?

No, it's a browser API. On a Node.js backend, capture webpages by driving a browser with Playwright or Puppeteer, or by calling a screenshot API.

Read more Desktop screen capture

Interviews, tips, guides, industry best practices, and news.

View all posts

Automate website screenshots

Exhaustive documentation, ready SDKs, no-code tools, and other automation to help you render website screenshots and outsource all the boring work related to that to us.