How to Capture a Website Screenshot with Node.js on Mac
Use Puppeteer on macOS to capture a website screenshot with Node.js. Learn setup, viewport and full-page options, troubleshooting, and a no-browser API alternative.
To capture a website screenshot with Node.js on a Mac, use Puppeteer: install it, launch its bundled browser, navigate to the page, save the screenshot, and close the browser. The essential call is page.screenshot({ path: 'screenshot.png' }). This guide covers viewport, full-page and element captures, loading and configuration choices, troubleshooting, and an API option that does not require local browser setup.
1. Set up Puppeteer on macOS
Puppeteer is a Node.js browser automation library. The full puppeteer package normally downloads a compatible Chrome for Testing browser and a headless shell during installation. The browser download is approximately 170 MB for macOS according to the current installation guide; actual size and installation behavior can change by version.
- Open Terminal and create or enter a project directory.
- Initialize a Node.js project if you do not already have one.
- Install Puppeteer and let its install step download the browser.
- Create the script below and run it with Node.js.
mkdir website-shot
cd website-shot
npm init -y
npm install puppeteer
The examples use ES modules. Add "type": "module" to package.json, or save the script as screenshot.mjs. If your project uses CommonJS, see the note below.
2. Capture a website screenshot
Save this as screenshot.js in a project whose package.json sets "type": "module". It captures the visible browser viewport of the page and writes a PNG file into the current directory.
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 60_000,
});
await page.screenshot({ path: 'screenshot.png' });
console.log('Saved screenshot.png');
} finally {
await browser.close();
}
Run it with an optional target URL:
node screenshot.js https://example.com
The try/finally ensures that the browser is closed even if navigation or capture fails. Choose a URL you are allowed to access, and remember that a page may display different content to an automated browser than it does to a person.
CommonJS alternative
If the project uses CommonJS, use require and an async function instead of top-level await:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 60_000,
});
await page.screenshot({ path: 'screenshot.png' });
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
3. Choose what to capture
Viewport screenshot
The default screenshot is the current viewport. Set its dimensions before navigation if the target layout depends on the screen size:
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'viewport.png', type: 'png' });
width and height are CSS pixels. A larger deviceScaleFactor requests more image pixels for the same CSS viewport and increases output size and capture work. Use values appropriate to your output or visual comparison setup.
Full-page screenshot
To capture the page’s full scrollable height rather than only the viewport, set fullPage: true:
await page.screenshot({ path: 'full-page.png', fullPage: true });
Full-page capture can produce a tall, memory-intensive image on long pages. Some pages load more content as you scroll, so a full-page option alone may not trigger every lazy-loaded image or feed item. If the target requires scrolling to load content, scroll in steps and wait for its content before capturing. Pages with sticky headers, animations, or scroll-triggered effects may render differently when combined into a tall screenshot.
Screenshot one element
Wait for the target element, then use its element handle’s screenshot method. Puppeteer attempts to scroll a hidden element into view:
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const element = await page.waitForSelector('main article', { timeout: 15_000 });
if (!element) throw new Error('Target element was not found');
await element.screenshot({ path: 'article.png' });
Use a selector that identifies one element reliably. If the selector matches an unexpected element, refine it or inspect the page structure. Element screenshots capture the element’s rendered bounds; they do not mean the whole page is captured.
Return image bytes instead of writing a file
When another part of the program will upload or process the image, capture a buffer and pass it along without first writing a local file:
const image = await page.screenshot({ type: 'png' });
// Pass image (a Buffer) to your storage or image-processing code.
4. Wait for the page to be ready
Navigation completion does not guarantee that all visible content has finished rendering. Puppeteer’s page.goto() supports different waitUntil conditions:
| Condition | Use | Trade-off |
|---|---|---|
load |
Wait for the load event, including dependent resources such as stylesheets and images. | Pages with long-running requests or delayed content may need an additional targeted wait. |
domcontentloaded |
Continue when the initial document has been parsed. | Images, fonts, and application-rendered content may still be loading. |
networkidle0 |
Wait until there are no more than zero network connections for the specified idle period. | Analytics, polling, or other persistent requests may prevent the condition from being reached. |
networkidle2 |
Wait until there are no more than two network connections for the idle period. | It still cannot guarantee that every site-specific visual update is complete. |
For a page with a known content marker, waiting for that selector is often more direct than waiting for all network activity:
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-page-ready="true"]', { timeout: 15_000 });
await page.screenshot({ path: 'ready.png' });
Use a selector that actually exists on the target site. If you need a short settling delay for a known animation or delayed render, add a bounded wait, but avoid arbitrary long sleeps when a selector or other explicit readiness signal is available.
5. Control output and browser behavior
Image type and quality
Puppeteer can write PNG, JPEG, or WebP screenshots. Specify type when the output format matters. JPEG and WebP support a quality setting; PNG is lossless and does not use that lossy quality control.
await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 85 });
await page.screenshot({ path: 'page.webp', type: 'webp', quality: 85 });
Use PNG for crisp text and pixel comparison workflows. Use JPEG or WebP when smaller lossy output is acceptable. Confirm format support in the browser version used by your project.
Background transparency
For a transparent screenshot, the page must have a transparent background. With a PNG output, you can omit the default white background:
await page.screenshot({ path: 'transparent.png', type: 'png', omitBackground: true });
Browser launch and browser management
The standard package-managed setup uses puppeteer.launch() and the browser binary downloaded by Puppeteer. If you use puppeteer-core, it does not download Chrome; you must manage the browser yourself and configure its executable path or channel. That can suit environments that provide a browser already or connect to a remote browser, but it adds setup responsibility.
On macOS, install scripts may be blocked by package-manager policy. Puppeteer’s documented recovery is:
npx puppeteer browsers install
This installs the browser binaries required by the installed Puppeteer package. Keep the Puppeteer and browser versions aligned when you manage browser installation yourself.
Cookie banners and other overlays
A local browser screenshot shows the page state the browser receives. Cookie consent dialogs, newsletter popups, and chat widgets may cover content. If you control the site, configure its test state or dismiss the dialog through the site’s legitimate interface before capture. For a page you do not control, do not assume the overlay can be reliably removed by generic browser code.
6. Puppeteer or Playwright?
Both Puppeteer and Playwright document Node.js screenshot workflows. Puppeteer is a straightforward option when you want its browser automation API and package-managed Chrome setup. Playwright’s page API supports screenshots with Chromium, Firefox, or WebKit, and can fit a project already using Playwright Test. Choose based on the browser engines, test tooling, and setup your project needs; the cited documentation does not establish that either is universally faster on Mac.
Playwright’s basic pattern is to launch a browser, open a page, navigate, and call screenshot(). For example, with its Chromium package installed:
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'playwright-shot.png', fullPage: true });
} finally {
await browser.close();
}
Install the package and browser required by your chosen Playwright setup, following its official installation instructions. Its full-page option captures the scrollable page as if it were displayed on a very tall screen.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Could not find Chrome |
Install scripts were skipped or the browser binary was not downloaded. | Run npx puppeteer browsers install. Check whether your package manager or deployment policy blocks install scripts. |
SyntaxError near import or top-level await |
The project is running the script as CommonJS. | Add "type": "module" to package.json, use a .mjs extension, or use the CommonJS example. |
| Navigation times out | The site is slow, a request remains open, or the selected network-idle condition never occurs. | Set an intentional timeout, use domcontentloaded or load, then wait for the specific content needed. Do not silently treat a timed-out navigation as a complete page. |
| Screenshot misses content or shows a spinner | The app renders content after navigation or waits on client-side work. | Wait for a content selector or a site-specific readiness signal before capture. For lazy content, scroll as needed and wait for it to load. |
| Element selector times out | The selector is wrong, the element is inside a different frame, or it appears only after interaction. | Verify the selector in the page, wait for the relevant state, and use the correct frame or interaction flow. |
| Unexpected page layout | Viewport, device scale, browser, fonts, or page state differs from the intended capture. | Set the viewport explicitly and use consistent browser and environment settings when comparing images. |
| Browser process remains after an error | Cleanup did not run after a failed operation. | Put browser work in try/finally and call browser.close() in the finally block. |
8. Performance, reliability, and cost
- Startup: launching a browser has overhead. For a batch, consider reusing one browser process and opening a page per URL, while closing each page and the browser when finished.
- Memory: full-page captures and high device scale factors can produce large images and use more memory. Prefer viewport screenshots when the whole document is unnecessary.
- Waiting: a broad network-idle wait can be slow or time out on sites with persistent requests. A specific selector is often a clearer completion condition.
- Repeatability: screenshot output can vary with macOS version, browser version, settings, hardware, power source, and headless mode. Keep the capture environment consistent for visual baselines and review image changes instead of assuming every pixel difference is a product defect.
- Local costs: Puppeteer itself is an open-source dependency, but the full setup downloads browser binaries and uses local CPU, memory, disk, and engineering time. The approximate browser download size is version-sensitive. The cited sources do not establish a Mac performance benchmark or a universal runtime cost.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request returns a PNG, JPEG, WebP, or PDF without installing or managing a browser on your Mac. See the ScreenshotNeo API documentation.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'screenshot.png' });
} finally {
await browser.close();
}
Or make the capture with one API call. Replace YOUR_API_KEY with your ScreenshotNeo key and change the target URL as needed:
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie banners are accepted like a visitor, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot. Each of these steps can be turned off.
- Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers identify the page verdict and whether the shot was billed.
- An MCP server lets AI agents using Claude, Cursor, or another MCP client call
take_screenshot,get_page_info, andcapture_pdf. - The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.
FAQ
Can I take a screenshot without saving it to disk?
Yes. Ask Puppeteer for screenshot bytes without a path and pass the returned buffer to your upload or processing code.
Will the screenshot look exactly like what I see in Safari?
Not necessarily. Puppeteer uses its configured browser, and rendering can differ across browser engines and environments. Use a consistent browser and settings when appearance must be repeatable.
Does full-page mode load every image on the page?
It captures the scrollable page, but a site’s lazy-loading behavior may require scrolling and waiting before capture.
Can I capture a page that requires authentication?
Browser automation can interact with a site you are authorized to access, but this basic example does not log in. Use the site’s permitted authentication flow and protect any credentials used by automation.
Which library should I start with?
Start with Puppeteer for its direct Chrome setup, or Playwright if your project already relies on its cross-browser or test tooling. Neither library is established by the cited sources as the best choice for every Mac workflow.


