ScreenshotNeo

BlogHow-to

How to Capture a Screenshot After a Button Click with Puppeteer

Click a button, wait for the resulting page state, and capture it with Puppeteer. Includes navigation, dynamic UI, element screenshots, troubleshooting, and an API alternative.

By the ScreenshotNeo team4 October 20267 min read

To capture the page after a button click with Puppeteer, click the intended button, wait for the post-click state you need, then await page.screenshot(). For a button that updates the current page, use a locator and wait for an observable result:

await page.locator('button.save').click();
await page.waitForSelector('.success-message', { visible: true });
await page.screenshot({ path: 'after-click.png' });

If the click navigates, start page.waitForNavigation() and the click together with Promise.all(). Starting the navigation wait after clicking can miss a fast navigation. These examples follow the Puppeteer interactions and API documentation; replace the example URL, selectors, and state condition with ones from your own page.

1. Set up a runnable Puppeteer script

Install Puppeteer in a Node.js project:

npm install puppeteer

Save this as capture-after-click.js. It opens a page, waits for a button, clicks it, waits for the resulting message, and writes a screenshot:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

    await page.locator('button.save').click();
    await page.waitForSelector('.success-message', { visible: true });
    await page.screenshot({ path: 'after-click.png', fullPage: true });
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The selectors are placeholders: choose a locator for the intended button and an observable result that means the page is ready for your screenshot. If the page requires authentication, set up the required session or cookies before clicking.

2. Choose the right click and wait pattern

Button updates the current page

Wait for the UI change that matters to the screenshot. A visible confirmation, expanded panel, changed heading, or updated attribute can be a useful readiness condition.

await page.locator('button.save').click();
await page.waitForSelector('.success-message', { visible: true });
await page.screenshot({ path: 'saved.png' });

waitForSelector() returns immediately if the selector already exists. For state changes where an existing element changes rather than appearing, wait for a more specific condition with page.waitForFunction() instead.

Button triggers navigation

Begin waiting for navigation before the click can trigger it. Run both operations in the same Promise.all():

const [response] = await Promise.all([
  page.waitForNavigation(),
  page.locator('button.continue').click(),
]);
await page.screenshot({ path: 'destination.png', fullPage: true });

waitForNavigation() resolves with the navigation response, or null for some same-document navigations. Choose navigation options that fit the site. A button that updates content without navigating should use a UI-state wait instead.

Wait for a specific condition

Use waitForFunction() when readiness is best expressed as a condition in the page. The following waits until a button’s aria-expanded attribute becomes true:

await page.locator('button.menu').click();
await page.waitForFunction(() => {
  const button = document.querySelector('button.menu');
  return button?.getAttribute('aria-expanded') === 'true';
});
await page.screenshot({ path: 'menu-open.png' });

Use a condition tied to the state you need to show. A fixed delay can be useful for a known animation, but it does not confirm that an application update succeeded:

await page.locator('button.open').click();
await new Promise((resolve) => setTimeout(resolve, 500));
await page.screenshot({ path: 'after-animation.png' });

Capture only the relevant element

If the screenshot only needs to show a component, wait for it and capture its element handle:

const result = await page.waitForSelector('.result-panel', { visible: true });
if (!result) throw new Error('Result panel was not found');
await result.screenshot({ path: 'result-panel.png' });

An element screenshot keeps the image focused on that component. ElementHandle.screenshot() attempts to scroll the element into view if needed. Use a page screenshot when surrounding context matters.

3. Select the intended button reliably

Puppeteer recommends locators for selecting and interacting with elements. A locator click waits for useful preconditions, including visibility, enabled state, and a stable bounding box. Use a selector that identifies one control clearly:

await page.locator('button[data-action="save-profile"]').click();

CSS selectors work, and Puppeteer supports additional locator syntax, including text and accessibility-based selectors. Avoid broad selectors such as button when the page has several buttons: clicking the wrong match can produce a valid screenshot of the wrong state.

page.click(selector) is also available. It finds the selector, scrolls the first matching element into view if needed, and clicks its center. Prefer a specific selector either way.

4. Configure the screenshot

page.screenshot() returns image data by default and can write directly to a file with path. For example:

await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'full-page.png', fullPage: true });

Use fullPage: true when the entire document matters; omit it to capture the current viewport. For a buffer rather than a file, omit path. The documented default return is a Uint8Array; with encoding: 'base64', the return value is a string:

const imageData = await page.screenshot();
const base64 = await page.screenshot({ encoding: 'base64' });

Pick page or element capture according to the evidence the image needs to preserve:

Situation Wait for Capture
Button navigates to another page waitForNavigation() started together with the click page.screenshot()
Current page changes state A result selector or waitForFunction() condition page.screenshot()
Only a component matters The target element to become visible ElementHandle.screenshot()

5. Timeouts, errors, and troubleshooting

Symptom Likely cause Fix
Screenshot shows the old state Capture ran before the asynchronous UI update finished. Wait for a selector or condition that represents the new state, then capture.
Navigation wait times out or misses the destination The wait started after the click, or the button did not navigate. For navigation, start the wait and click together in Promise.all(). If the page updates in place, wait for that UI state instead.
Click times out The locator did not become actionable, the selector is wrong, or the target is disabled or obscured. Check the selector and page state; wait for the intended control to be visible and enabled. Set an appropriate timeout if the page legitimately needs more time.
Wrong button was clicked The selector matched multiple controls. Use a unique class, attribute, text, or accessibility locator for the intended button.
Element screenshot fails or target is missing The result selector did not appear, or a hidden-selector wait returned no element. Wait for the visible target and check the returned handle before calling screenshot().
Wait reaches its timeout despite the UI looking ready The wait condition describes presence rather than the actual visual state, or the page uses a different state signal. Inspect the page’s DOM and wait for the exact changed text, attribute, visibility, or other state that indicates readiness.

waitForSelector() documents a default timeout of 30 seconds. Configure a longer or shorter timeout per wait, or set a page default with page.setDefaultTimeout(). Locator actions inherit the page timeout and can also use locator-specific timeout settings. Avoid increasing timeouts to hide a selector or synchronization bug.

6. Performance, reliability, and cost

  • Wait for the smallest meaningful condition. A specific result selector usually avoids waiting longer than necessary. A fixed delay can make runs slower while still failing on a slower page.
  • Choose navigation readiness deliberately. Waiting for a navigation event establishes that navigation occurred; it does not guarantee every later application-rendered detail is ready. If the screenshot depends on a specific destination element, wait for that too.
  • Keep the browser lifecycle bounded. Close the browser in a finally block so an exception during navigation, clicking, waiting, or capture does not leave the process running.
  • Plan for page variability. Network speed, animations, authentication, and application state can affect when a result appears. Use observable conditions and set timeouts to fit the workflow.
  • Account for browser infrastructure. A local Puppeteer script requires a Node.js runtime and browser installation/runtime support. For repeated or parallel captures, account for browser process memory, startup time, and concurrency in your own environment.
  • Cost depends on where it runs. Puppeteer itself is an automation library; the runtime and infrastructure you choose may have costs. There is no universal cost or performance figure for this workflow.

Or skip the browser setup

If you only need a screenshot after the page has loaded, ScreenshotNeo offers a one-request screenshot API. See the ScreenshotNeo API documentation for request options. This call saves the returned image:

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

ScreenshotNeo accepts cookie banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides screenshot tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

FAQ

Can I take the screenshot immediately after clicking?

Yes, if the click has already completed the state change the image needs to show. For asynchronous updates, wait for an observable result first.

Can Puppeteer capture a screenshot as data instead of a file?

Yes. Call page.screenshot() without a path to get image data, or set encoding: 'base64' for a base64 string.

Should I use a page screenshot or an element screenshot?

Use a page screenshot when page context matters and an element screenshot when the component alone is the evidence you need.

Does every button click trigger navigation?

No. Many buttons update the current page. Match the wait to the observed behavior: navigation for a document change, or a selector or condition for an in-page update.