Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use page.setContent(html) when each loop iteration supplies a complete HTML document; use page.evaluate() when you need to change part of an already loaded page. Await the update and screenshot in each iteration, and give every image a unique filename. The examples below show both approaches with Puppeteer and Playwright.

Choose whether to replace the document or update the page

The right method depends on what changes between screenshots:

  • Each state is a standalone HTML document: render that state as HTML and call page.setContent(html) before capturing it.
  • The page is already loaded and only some content changes: call page.evaluate() to update the relevant DOM or application state, then capture.

setContent is not just a shortcut for changing one element. Playwright documents that it uses document.write() semantics, so it replaces the page content rather than preserving a live application as-is. See the Playwright Page API. Puppeteer also exposes setContent for setting page content in its Page.setContent API reference.

Set up a Node.js project

Choose one library for the script. Both examples below use a locally launched browser and write numbered PNG files into a screenshots directory.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Install Puppeteer

npm init -y
npm install puppeteer

Or install Playwright

npm init -y
npm install playwright
npx playwright install chromium

The install command obtains Playwright’s Chromium browser. If your project already provisions browsers in its environment, follow that environment’s setup instead. Puppeteer and Playwright APIs evolve; check the linked official references when matching code to a particular installed version.

Capture a screenshot for each complete HTML state

This Puppeteer script creates a page once, sets a viewport, replaces its content for each input, and waits for the screenshot before moving to the next item. Save it as capture.js, then run node capture.js.

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

const items = [
  { title: 'First card', color: '#e9f2ff' },
  { title: 'Second card', color: '#fff1df' },
  { title: 'Third card', color: '#e8f7ec' },
];

function escapeHtml(value) {
  return String(value).replace(/[<>&"']/g, (character) => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
  })[character]);
}

function renderHtml(item) {
  return `<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body { margin: 0; font: 16px system-ui, sans-serif; }
      main { box-sizing: border-box; min-height: 100vh; padding: 48px; background: ${item.color}; }
      h1 { margin: 0; }
    </style>
  </head>
  <body><main><h1>${escapeHtml(item.title)}</h1></main></body>
</html>`;
}

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage({
      viewport: { width: 1280, height: 800 },
    });
    await fs.mkdir('screenshots', { recursive: true });

    for (let i = 0; i < items.length; i++) {
      await page.setContent(renderHtml(items[i]));
      const filename = `screenshots/shot-${String(i + 1).padStart(3, '0')}.png`;
      await page.screenshot({ path: filename });
      process.stdout.write(`Saved ${filename}n`);
    }
  } finally {
    await browser.close();
  }
})();

The escaping helper treats each title as text rather than trusted markup, preventing characters such as < or & from being interpreted as HTML. In real applications, use your existing HTML templating and escaping strategy. The code keeps the viewport constant, so each capture uses the same visible dimensions. To include content beyond the viewport, use Puppeteer’s fullPage screenshot option, described in its screenshots guide.

Playwright version

The same loop works with Playwright. Replace the Puppeteer import and launch/page setup with Playwright’s API; here is a complete equivalent to save as capture-playwright.js:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const fs = require('node:fs/promises');
const { chromium } = require('playwright');

const items = [
  { title: 'First card', color: '#e9f2ff' },
  { title: 'Second card', color: '#fff1df' },
  { title: 'Third card', color: '#e8f7ec' },
];

function escapeHtml(value) {
  return String(value).replace(/[<>&"']/g, (character) => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
  })[character]);
}

function renderHtml(item) {
  return `<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body { margin: 0; font: 16px system-ui, sans-serif; }
      main { box-sizing: border-box; min-height: 100vh; padding: 48px; background: ${item.color}; }
      h1 { margin: 0; }
    </style>
  </head>
  <body><main><h1>${escapeHtml(item.title)}</h1></main></body>
</html>`;
}

(async () => {
  const browser = await chromium.launch({ headless: true });
  try {
    const page = await browser.newPage({
      viewport: { width: 1280, height: 800 },
    });
    await fs.mkdir('screenshots', { recursive: true });

    for (let i = 0; i < items.length; i++) {
      await page.setContent(renderHtml(items[i]));
      const filename = `screenshots/shot-${String(i + 1).padStart(3, '0')}.png`;
      await page.screenshot({ path: filename });
      process.stdout.write(`Saved ${filename}n`);
    }
  } finally {
    await browser.close();
  }
})();

Playwright’s page.screenshot() accepts a path and exposes options including image type and full-page capture; see the Page API. The two scripts are alternatives, not code to run together.

Update an existing page without replacing its HTML

For a page that should retain its document and change one value per iteration, use evaluate. The function runs in the browser page context; ordinary variables from Node.js are not automatically visible there. Pass each changing item as an argument. Puppeteer documents argument passing and asynchronous evaluation in its Page.evaluate API; Playwright explains the separation between Node.js and page contexts in Evaluating JavaScript.

for (let i = 0; i < items.length; i++) {
  await page.evaluate((item) => {
    document.querySelector('#preview').textContent = item.label;
    document.querySelector('#preview').style.backgroundColor = item.color;
  }, items[i]);

  const filename = `screenshots/shot-${String(i + 1).padStart(3, '0')}.png`;
  await page.screenshot({ path: filename });
}

This snippet assumes page is already open on a document containing an element with id="preview", and items contains label and color values. If the selector does not match anything, the page-side code needs to handle that case or the update will fail. For application frameworks, changing the framework’s state through its supported interface is generally safer than making direct DOM edits that the framework may overwrite.

Make each capture represent the intended visual state

Awaiting setContent or evaluate and then awaiting screenshot prevents the loop from advancing before those operations finish. It does not guarantee that every external font, image, stylesheet, animation, or application request has reached the exact visual state you want. The APIs do not establish one readiness rule that fits every page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Static, self-contained HTML: set the content and capture it. Inline CSS and data that do not depend on external resources make the result easier to reproduce.
  • External images or fonts: wait for the specific resource or a page-side readiness signal before capturing. For images, you can check that relevant elements are complete and have natural dimensions; decide how to treat broken images instead of waiting indefinitely.
  • Dynamic application: wait for an app-specific selector or explicit ready marker that appears only after the state is rendered. A fixed sleep can be too short on a slow run and unnecessarily long on a fast one.
  • Navigation to a URL: wait for a condition appropriate to that site. Puppeteer’s screenshot guide demonstrates navigation with page.goto(url, { waitUntil: 'networkidle2' }), but that is an example for navigation, not a universal rule for every page or for setContent.

If the page uses animation or time-dependent content, make that deterministic where possible—for example, disable animation in the capture stylesheet or freeze the data used to render the state. Otherwise two screenshots of the same input may differ even though the loop is working as written.

Choose viewport, full-page, or element capture

Capture goal Approach Trade-off
Visible viewport Set the viewport and call page.screenshot({ path }). Content below the viewport is not included.
Entire page Use the library’s full-page screenshot option. Playwright documents fullPage in its Page API. A long document produces a taller image; it is not the same as a viewport capture.
One element Locate the element and capture its bounding box or use the library’s element screenshot API. Puppeteer’s screenshot guide demonstrates element screenshots. The target must exist and be visible in the state being captured.

For device-sized output, set the viewport to the dimensions you want before rendering. A device scale factor changes pixel density; it does not change the CSS layout dimensions in the same way as choosing a different viewport. Puppeteer and Playwright expose different option details, so consult the relevant API for the installed library rather than copying an option name between them.

Keep loop output ordered and recover cleanly from errors

Use for with await when each state must be captured before the next is applied. Avoid items.forEach(async item => ...) when capture order matters: the outer function does not wait for those callbacks. Every output path should be unique. Screenshot APIs accept paths, but do not enforce uniqueness, so reusing a filename overwrites an earlier image.

A browser in a finally block closes even if rendering or saving throws. For batch work where one bad item should not stop the rest, wrap the work for each item in its own try/catch, record the failed item’s index and error, and decide whether to continue; keep the outer finally around the browser close. Do not mutate the same page from concurrent tasks. If you need parallel work, use separate pages and distinct output paths, and account for the extra browser resources.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common loop failures

Every output file contains the same state

Check that the state update is awaited and completes before the screenshot call. In an existing page, confirm the selector matches and the application has rendered the new value. Do not fire asynchronous callbacks with forEach and assume the loop waits for them.

Some images or fonts are missing

The content-setting call can finish before external assets have produced the appearance you expect. Wait for the specific assets or a known ready condition, and check that the browser can access their URLs. An image that is blocked or returns an error will not be repaired by increasing an arbitrary fixed delay.

The page is blank or content appears clipped

Verify that the generated HTML includes the expected body content, that the viewport matches the intended capture, and that you chose viewport rather than full-page capture when content extends below the fold. For a targeted capture, check that the element exists and is visible.

Earlier screenshots disappear

Make the filename depend on the iteration or a stable item identifier. If filenames can contain user-provided text, sanitize that text before using it as a path. A unique name per item avoids accidental replacement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The process remains open after a failure

Put browser.close() in a finally block that runs after the browser is successfully launched. If launch itself fails, there is no browser instance to close; if the failure occurs inside the loop, finally still handles cleanup.

Or skip the browser setup

If you need a hosted screenshot of a URL rather than a local browser loop that mutates arbitrary page state, ScreenshotNeo is a website screenshot API and MCP server. It removes cookie-consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are not billed. Its MCP server provides screenshot tools for AI agents. The API call below saves a screenshot response; see the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

For a one-off request, ScreenshotNeo includes 1,000 screenshots per month on its free plan with no card required; paid plans start at $5 for 3,000. This API call captures a URL; it is not a substitute for changing arbitrary DOM state in a locally controlled page. Sign up for the free plan.

Documentation version context

The Puppeteer setContent page displayed version 25.11.0 in the reviewed documentation, while its screenshot guide and screenshot reference displayed 25.12.0. Those page labels should not be read as a synchronized release statement. The reviewed Playwright Page API is rolling documentation, and the evaluating-JavaScript page is labeled “Next.” Check the documentation corresponding to the package version in your project before depending on version-specific options.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.