ScreenshotNeo

BlogHow-to

How to Capture Scheduled Website Screenshots After Dismissing a Modal

Use Playwright to dismiss a modal, wait for the page state you need, and capture a screenshot on a schedule. Includes runnable code and troubleshooting.

By the ScreenshotNeo team4 October 202610 min read

To capture a scheduled website screenshot after dismissing a modal, run a browser automation script on a schedule. In the script, open the page, wait for the expected modal, click its real dismiss control, confirm it is gone, wait for the content you need, and save a viewport, full-page, or element screenshot.

This example uses Playwright with Node.js. It handles a predictable HTML modal; replace the example URL and selectors with stable selectors from your own page. The scheduler runs the script repeatedly, while the script decides when the page is ready to capture.

1. Set up Playwright

mkdir scheduled-shot
cd scheduled-shot
npm init -y
npm install playwright
npx playwright install chromium

Save the following as capture.mjs. It accepts the target URL from the TARGET_URL environment variable, so the same script can be reused by a local scheduler or CI runner.

import { chromium } from 'playwright';

const url = process.env.TARGET_URL ?? 'https://example.com';
const output = process.env.OUTPUT ?? 'screenshot.png';
const browser = await chromium.launch({ headless: true });

try {
  const page = await browser.newPage({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1,
  });

  // A page-level timeout is a ceiling, not a readiness condition.
  page.setDefaultTimeout(10_000);
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });

  // Replace these with the modal's stable, site-specific selectors.
  const modal = page.locator('[role="dialog"]');
  const dismiss = modal.getByRole('button', { name: /close|accept|dismiss|continue/i });

  // If the modal is expected on every run, wait for it explicitly.
  await modal.waitFor({ state: 'visible', timeout: 10_000 });
  await dismiss.click();
  await modal.waitFor({ state: 'hidden', timeout: 10_000 });

  // Wait for the actual content that should appear in the screenshot.
  await page.locator('main').waitFor({ state: 'visible', timeout: 15_000 });

  await page.screenshot({ path: output, fullPage: true, animations: 'disabled' });
  console.log(`Saved ${output}`);
} finally {
  await browser.close();
}

Run it once before scheduling it:

TARGET_URL=https://example.com node capture.mjs

If the modal does not always appear, do not make the whole capture fail just because it is absent. Check its visibility with a short, bounded wait, dismiss it if present, and continue to the page-specific readiness condition. The selectors above are examples: inspect the page and use the modal’s actual accessible name or a stable attribute.

2. Choose the right modal handling

For a predictable overlay, explicitly wait for it and dismiss it in the normal flow. Playwright recommends this over page.addLocatorHandler() when the overlay appears predictably. A locator handler is intended for unexpected overlays; it runs before actions or auto-waiting assertions, and its time counts against that action or assertion’s timeout. It can also change focus or mouse state, so actions that depend on those should be resilient. Playwright locator handler documentation.

For an optional modal, use a bounded check rather than waiting the full timeout unconditionally:

const modal = page.locator('[role="dialog"]');
try {
  await modal.waitFor({ state: 'visible', timeout: 2_000 });
  await modal.getByRole('button', { name: /close|accept|dismiss/i }).click();
  await modal.waitFor({ state: 'hidden', timeout: 5_000 });
} catch (error) {
  // Continue only when the modal was simply absent.
  // Do not swallow a failed click or a modal that remains open.
  const stillVisible = await modal.isVisible().catch(() => false);
  if (stillVisible) throw error;
}
await page.locator('main').waitFor({ state: 'visible', timeout: 15_000 });

Keep the absence case distinguishable from a broken dismissal. For production use, consider a dedicated helper that treats only the initial visibility timeout as “not present,” then lets click failures and failure to become hidden stop the capture.

A JavaScript alert, confirm, prompt, or beforeunload dialog is not an HTML modal. If you register a Playwright dialog listener, accept or dismiss the dialog in the listener or the triggering action can stall. Without a listener, Playwright automatically dismisses dialogs. Example, when acceptance is correct for your task:

page.on('dialog', async dialog => {
  await dialog.accept();
});

Choose dialog.dismiss() instead if the intended behavior is to cancel. Do not install a blanket accept handler without considering the site’s behavior: accepting a confirmation may submit an action.

3. Wait for the page state you need

domcontentloaded only indicates that the initial document has been parsed. It does not guarantee that a single-page app has rendered its data, images have loaded, or a modal has finished animating. Prefer a condition tied to the page and screenshot, such as a content locator, a known heading, or the disappearance of a loading indicator.

// Examples: choose conditions that match the page being captured.
await page.getByRole('heading', { name: 'Monthly report' }).waitFor({ state: 'visible' });
await page.locator('[data-state="loading"]').waitFor({ state: 'hidden' });
await page.locator('.chart canvas').waitFor({ state: 'visible' });

A fixed delay can be useful for a known animation or delayed widget, but it is a weak substitute for readiness: network and rendering times vary. If a delay is genuinely required, keep it short and pair it with a meaningful condition.

4. Capture the viewport, full page, or one element

Playwright supports page screenshots and locator screenshots. Pick the smallest capture scope that meets the need. See the official screenshot guide and locator screenshot API.

Need Example Trade-off
Visible viewport await page.screenshot({ path: 'view.png' }) Fast and bounded, but excludes content below the fold.
Entire scrollable page await page.screenshot({ path: 'full.png', fullPage: true }) Includes the full page; long pages can take more time and memory.
One component await page.locator('.report-card').screenshot({ path: 'card.png' }) Focused output; the locator must resolve to the intended visible element.

For element capture, wait for the element to be visible and stable, and ensure overlays do not cover it. For full-page captures, lazy-loaded sections may need to be scrolled into view before capture so their content is loaded; do this only if the page’s behavior requires it.

5. Run the capture on a schedule

The scheduler is environment-specific. Configure it to invoke the script at the desired interval and provide TARGET_URL and an output destination. For example, a server’s cron service, a CI scheduler, or a managed job runner can launch node capture.mjs. Confirm the selected scheduler’s current syntax, timezone rules, concurrency behavior, and retention policy in its own documentation.

For a recurring job, make each run independent: create a fresh browser context, write output to a unique filename or upload it to a destination that supports the retention you need, and return a non-zero exit code when navigation, modal dismissal, readiness, or capture fails. Avoid overlapping runs if the destination uses a single fixed output filename.

Example shell wrapper with a timestamped local output:

#!/usr/bin/env bash
set -euo pipefail
stamp=$(date -u +%Y%m%dT%H%M%SZ)
TARGET_URL="https://example.com" OUTPUT="shots/site-$stamp.png" node capture.mjs

Create the shots directory before scheduling or add mkdir -p shots to the wrapper. Keep credentials in the scheduler’s secret store rather than in a checked-in script or command history.

6. Make recurring captures reliable

  • Keep the environment consistent. Browser version, operating system, hardware, and headless settings can affect rendering. Pin the Playwright/browser setup used for visual comparisons and run on a consistent host where practical. Playwright visual comparison guidance.
  • Control dynamic content. Timestamps, rotating banners, animations, and personalized content can make each image differ. Disable animations in screenshots where suitable and consider a page stylesheet to hide known volatile regions.
  • Use explicit timeouts. Set navigation, modal, readiness, and overall job limits. A timeout should identify which step failed rather than silently save a partial image.
  • Log useful context. Record the target URL, run time, browser version, output path, and failure stage. Avoid logging secrets or sensitive page content.
  • Retry selectively. A transient navigation failure may merit a limited retry. A selector mismatch or a modal that cannot be dismissed usually needs a code or page change; repeated retries only add delay.
  • Protect the destination. Ensure the scheduled identity can write the output and that retention does not grow without bound.

7. Python and cURL alternatives

The modal-aware do-it-yourself example above is in Node.js. The same browser steps can be implemented with Playwright’s Python API. Install it with pip install playwright and playwright install chromium:

import os
from playwright.sync_api import sync_playwright

url = os.environ.get("TARGET_URL", "https://example.com")
output = os.environ.get("OUTPUT", "screenshot.png")

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    try:
        page = browser.new_page(viewport={"width": 1440, "height": 1000})
        page.set_default_timeout(10_000)
        page.goto(url, wait_until="domcontentloaded", timeout=30_000)

        modal = page.locator('[role="dialog"]')
        modal.wait_for(state="visible", timeout=10_000)
        modal.get_by_role("button", name="Close").click()
        modal.wait_for(state="hidden", timeout=10_000)
        page.locator("main").wait_for(state="visible", timeout=15_000)
        page.screenshot(path=output, full_page=True, animations="disabled")
    finally:
        browser.close()

cURL alone cannot execute page JavaScript or click an HTML modal; it fetches HTTP responses. It is suitable only when the site provides a screenshot endpoint or when you use a browser screenshot service. For a direct one-request capture, see the ScreenshotNeo option below.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request captures a URL as PNG, JPEG, WebP, or PDF. Its clean-shot flow accepts cookie or consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture.

Use the API key from your account. The parameter names used by other screenshot APIs also work. See the ScreenshotNeo API documentation.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
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(async fs => {
  await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
});

Schedule that request with the same kind of job runner as the Playwright script. ScreenshotNeo offers caching with a chosen TTL and asynchronous jobs with signed webhooks; use async jobs when they fit your schedule and delivery workflow. A single API call captures a page, while a scheduled runner is still needed to repeat it.

Plans include 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000. The Free plan is suitable for trying scheduled captures, and every feature is available on every plan. Create a free account and get 1,000 screenshots a month with no card.

Troubleshooting

Symptom Likely cause Fix
Modal wait times out The modal is optional, uses a different selector, or appears after a delay. Inspect the live DOM and accessible roles. For optional overlays, use a bounded visibility check; retain a hard failure when a visible modal cannot be dismissed.
Click times out or hits the wrong control The selector is ambiguous, the control is covered, or the button label differs. Scope the locator to the dialog, use its accessible role and actual name, and wait for the dialog to be visible.
Capture contains the overlay The click did not close it, or a transition is still running. Wait for the modal to reach hidden state after clicking; investigate if that condition times out instead of capturing anyway.
Screenshot is blank or missing content The script captured before app rendering or data loading finished. Wait for a page-specific heading, content element, or loading indicator to reach its final state.
Native dialog stalls an action A dialog listener was registered but did not accept or dismiss the dialog. Handle it in the listener with the intended action. Without a listener, Playwright dismisses dialogs automatically.
Scheduled run works locally but fails on the runner Different browser installation, environment variables, permissions, timezone, or output path. Install the matching browser, set variables in the job configuration, use an absolute or prepared output directory, and inspect the scheduler’s run logs.
Images differ between runs Browser or host differences, dynamic content, animations, fonts, or personalized data. Keep the rendering environment consistent, disable animations where appropriate, and filter known changing regions.
Runs overwrite each other Every run writes to the same filename while jobs overlap. Use unique timestamped names, prevent overlap in the scheduler, or deliberately define an overwrite policy.

Performance and cost considerations

Browser automation has setup and runtime costs: the job must launch a browser, load the page, wait for the modal and content, and write or upload the image. Full-page capture and pages with heavy assets generally require more work than a viewport or focused element. Keep the browser process and output handling appropriate to the job volume, and avoid waiting for unrelated resources when a page-specific readiness condition is sufficient.

A self-managed script has no screenshot API charge, but it uses runner capacity and requires browser installation, scheduling, storage, and maintenance. A managed API reduces browser setup and can provide capture options such as full-page screenshots, element selectors, custom CSS or JavaScript, wait conditions, caching, bulk capture, and signed links. ScreenshotNeo bills only clean shots; its response includes verdict and billing headers, so scheduled jobs can inspect what happened rather than infer billing from an image file.

FAQ

Should the script wait for the modal or wait for it to disappear?

For a modal expected on each run, wait for it to become visible, dismiss it, then wait for it to become hidden. For an optional modal, check briefly for visibility and continue if absent.

Can Playwright dismiss any website modal automatically?

No. An HTML overlay needs a page-specific action or selector. Native JavaScript dialogs use Playwright’s dialog handling and are a separate mechanism.

Can I save a screenshot every hour?

Yes. Configure your chosen scheduler to launch the script at that interval, and verify its timezone, overlapping-run behavior, and retention settings.

Does a full-page capture include content loaded only on scroll?

Not necessarily. If the page lazy-loads sections, scroll through the page before capturing and wait for those sections to render.