ScreenshotNeo

BlogScreenshots on your device

How to Take a Screenshot in Electron

Use Electron's capturePage() to save a window or region as PNG, understand scaling and desktop capture, and troubleshoot common failures.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: To screenshot the page rendered in an Electron BrowserWindow, call win.webContents.capturePage(). It returns a Promise for a NativeImage; call toPNG() and write the resulting Buffer with Node’s filesystem API. With no argument, Electron captures the whole visible page. Pass a rectangle to capture only a region.

This method captures your app’s rendered page. If you need the physical desktop or another application window, use Electron’s desktopCapturer with a media-capture API instead. Those are different capture targets with different permission and platform behavior.

Save an Electron window as a PNG

The following CommonJS example assumes you already have a BrowserWindow instance. It waits for the asynchronous capture, encodes the image as PNG, and writes it to disk.

const fs = require('node:fs/promises')

async function saveScreenshot(win, filePath) {
  const image = await win.webContents.capturePage()
  await fs.writeFile(filePath, image.toPNG())
}

// Example, after mainWindow has loaded its page:
await saveScreenshot(mainWindow, '/path/to/screenshot.png')

capturePage() is documented in Electron’s webContents API, and PNG encoding is provided by the NativeImage API.

Complete main-process example

This example creates a window, waits for its page to finish loading, then captures it. Run it from an Electron application’s main process.

const { app, BrowserWindow } = require('electron')
const fs = require('node:fs/promises')
const path = require('node:path')

async function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      contextIsolation: true
    }
  })

  await win.loadURL('https://example.com')

  const image = await win.webContents.capturePage()
  const outputPath = path.join(app.getPath('pictures'), 'electron-shot.png')
  await fs.writeFile(outputPath, image.toPNG())
  console.log(`Saved ${outputPath}`)

  return win
}

app.whenReady().then(createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

Keep the window alive until the Promise resolves. Closing or navigating the window during capture can make the result unusable or capture a different page than intended.

Capture only part of the page

Pass an object with x, y, width, and height as the first argument. The rectangle uses Electron’s Rectangle shape.

const image = await mainWindow.webContents.capturePage({
  x: 0,
  y: 0,
  width: 800,
  height: 600
})

await fs.writeFile('/path/to/region.png', image.toPNG())

The coordinates describe the rendered page area. A rectangle outside the currently rendered content may produce an empty or clipped result, so confirm the page’s viewport and layout before choosing coordinates.

Capture a DOM element by measuring it

capturePage() accepts a rectangle, rather than a CSS selector. To capture one element, first measure its bounding rectangle in the page, convert it to page coordinates, then pass those values to capturePage().

const bounds = await mainWindow.webContents.executeJavaScript(`
  (() => {
    const el = document.querySelector('.invoice')
    if (!el) throw new Error('Element .invoice was not found')
    const r = el.getBoundingClientRect()
    return {
      x: Math.round(r.left + window.scrollX),
      y: Math.round(r.top + window.scrollY),
      width: Math.round(r.width),
      height: Math.round(r.height)
    }
  })()
`)

const image = await mainWindow.webContents.capturePage(bounds)
await fs.writeFile('/path/to/invoice.png', image.toPNG())

This captures the element’s viewport-relative position after adding scroll offsets. Fixed-position elements, transforms, zoom, and a page that changes between measurement and capture can affect the result. For a stable export, disable animations and capture immediately after the final layout is ready.

Image dimensions and device scale factors

Electron returns an image at the page’s device scale factor. CSS dimensions are commonly expressed in device-independent pixels (DIP), while the PNG can contain more physical pixels on a high-density display. For example, a 800 by 600 DIP rectangle may produce a PNG wider than 800 physical pixels when the scale factor is greater than 1.

For offscreen rendering, the scale factor is controlled by webPreferences.offscreen.deviceScaleFactor. Account for this when allocating storage, comparing image dimensions, or sending screenshots to an image-processing pipeline.

Hidden windows and capture options

capturePage() also accepts optional capture settings. Electron documents stayHidden and stayAwake:

Option Use
stayHidden: true Keep a hidden BrowserWindow hidden during capture. Without it, Electron considers a hidden page visible while the capturer count is nonzero.
stayAwake: true Request that the page remain awake while the capture runs.

Use these options when running automated captures in a background window. A hidden page still needs to finish loading and render the state you intend to save.

Wait for the right visual state

capturePage() snapshots the current rendered state; it does not wait for your application’s data, fonts, images, or animations. Before calling it:

  1. Wait for did-finish-load or await loadURL().
  2. Wait for application data and critical images to be present.
  3. Disable transitions and animations if pixel stability matters.
  4. Scroll to the desired position before measuring a crop.
  5. Call capturePage() only after the final layout is visible.
await mainWindow.loadURL('https://example.com')
await mainWindow.webContents.executeJavaScript(`
  new Promise(resolve => {
    if (document.fonts) {
      document.fonts.ready.then(resolve)
    } else {
      resolve()
    }
  })
`)

const image = await mainWindow.webContents.capturePage()
await fs.writeFile('/path/to/ready.png', image.toPNG())

Whole desktop or another window: use desktopCapturer

If the requirement is “capture my monitor” or “capture another desktop window,” capturePage() is the wrong API. Electron’s desktopCapturer API lists media sources of type screen and window. You then select a source and use the appropriate media API to obtain a stream or video frame.

This produces a media-capture workflow rather than a still NativeImage export. On macOS 10.15 Catalina and later, screen capture requires user consent. On Linux using PipeWire, desktopCapturer.getSources() returns a single source; when both window and screen types are requested, PipeWire supports a single capture and the returned source is a window capture.

Electron also documents webContents.getMediaSourceId() for a WebContents stream used with getUserMedia and the tab source. That identifier is restricted to the requesting WebContents and remains valid for 10 seconds. It is useful for streaming workflows, not the simplest still-image export.

Common errors and fixes

Symptom Likely cause Fix
Blank or partially rendered image Capture ran before the page finished loading or before data/images were rendered. Await loadURL(), wait for your app’s ready condition, and capture after the final layout.
Wrong crop position Rectangle coordinates were measured in CSS pixels without considering scroll position or scale. Add window.scrollX/window.scrollY when measuring elements and account for the device scale factor in output dimensions.
File is empty or missing The asynchronous operation was not awaited, or the process exited first. Await both capturePage() and fs.writeFile() before closing the window or quitting.
Hidden window flashes on screen The capturer makes a hidden page visible during capture. Pass stayHidden: true and keep the window offscreen or otherwise isolated.
Screenshot shows an old state Capture happened before a React/Vue render, network response, or font load completed. Expose an application-ready signal, await it with executeJavaScript(), then capture.
Desktop capture is denied on macOS Screen-recording consent has not been granted. Request permission through the operating system and retry after consent. This applies to desktop media capture, not the app’s own capturePage() path.
Only one source appears on PipeWire Linux PipeWire supports a single capture source in this configuration. Design for the returned source and do not assume separate screen and window sources.

Performance, reliability, and storage

  • Reuse a window: Reusing one loaded BrowserWindow avoids repeated renderer startup when taking many screenshots.
  • Control output size: Retina scale factors increase PNG dimensions and memory use. Use a crop when a full window is unnecessary.
  • Prefer PNG for fidelity: toPNG() gives lossless PNG bytes. If you need another format, convert the bytes with an image library after capture.
  • Serialize state changes: Do not navigate, scroll, or mutate the DOM while the capture is in progress.
  • Handle failures: Wrap loading, JavaScript evaluation, capture, and file writes in try/catch; record the URL and rectangle with any failure.
  • Keep paths explicit: Use app.getPath() for portable locations such as the user’s Pictures directory rather than assuming a working directory.

Electron’s capture operation is asynchronous. Treat the returned Promise as the boundary for the screenshot lifecycle: await it, encode the resulting NativeImage, write the bytes, and only then report success.

Or skip the browser setup

If you need a URL screenshot without packaging an Electron window, ScreenshotNeo provides a GET endpoint that returns PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. See the ScreenshotNeo API docs for all options.

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()
open("shot.webp", "wb").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 failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Does capturePage() capture the whole scrollable page?

It captures the whole visible page by default. For a long document, scroll and capture sections yourself, or use a service designed for full-page capture.

Can I save JPEG or WebP directly from NativeImage?

The documented pattern here uses toPNG() to produce PNG bytes. Convert those bytes with an image-processing library if another output format is required.

Why is my PNG larger than the rectangle I passed?

The image uses the page’s device scale factor. Rectangle dimensions are in rendered coordinates, while the encoded image can contain more physical pixels on a high-density display.

Which API should I use for a screenshot of another application?

Use desktopCapturer and a media-capture API for desktop screens or windows. Use webContents.capturePage() for the page rendered by your own Electron window.