ScreenshotNeo

BlogHow-to

How to Click Elements Before Taking a Website Screenshot

Learn how to click a webpage element, wait for the resulting state, and capture the correct screenshot with Playwright, Puppeteer, or ScreenshotNeo.

By the ScreenshotNeo team1 October 20268 min read

To click an element before a website screenshot, await the click, wait for the state that proves the interaction finished, then capture the page or the specific element. In Playwright, a typical sequence is:

await page.getByRole('button', { name: 'Open details' }).click();
await expect(page.getByText('Details')).toBeVisible();
await page.screenshot({ path: 'after-click.png' });

The locator and the post-click condition must match the target page. A click completing does not always mean an application’s asynchronous update has finished.

1. Install Playwright and create a runnable script

Install Playwright in a new Node.js project:

mkdir screenshot-after-click
cd screenshot-after-click
npm init -y
npm install -D playwright
npx playwright install chromium

Save this as capture-after-click.js:

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });

  try {
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

    // Replace this locator with the control on the page you own or automate.
    const control = page.getByRole('button', { name: 'Open details' });
    await control.click();

    // Replace this assertion with the visible state caused by the click.
    await page.getByText('Details').waitFor({ state: 'visible' });

    await page.screenshot({ path: 'after-click.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

Run it with node capture-after-click.js. The example uses Playwright’s user-facing locator approach. Playwright documents locators as the basis of its auto-waiting and retry behavior, and recommends role, text, label, placeholder, alt text, title, and test-id locators where they describe what a user sees. See the Playwright locator guide and locator click API.

2. Choose a locator that survives page changes

Start with the most meaningful user-facing locator:

// Accessible role and name
page.getByRole('button', { name: 'Open details' })

// Visible text
page.getByText('Show pricing')

// Form label
page.getByLabel('Country')

// Placeholder
page.getByPlaceholder('Search documentation')

// Explicit test hook
page.getByTestId('open-details')

Role and accessible-name locators make the intended control clear. CSS selectors and XPath are useful when a page has no suitable accessible hook, but long chains tied to DOM structure are more likely to break when markup changes:

// More coupled to implementation details
page.locator('main > div:nth-child(2) button.details')

Use a CSS selector for a stable attribute when necessary:

await page.locator('[data-action="open-details"]').click();

When several controls match, narrow the locator with filter, first, or a parent region:

const card = page.getByRole('article').filter({ hasText: 'Pro plan' });
await card.getByRole('button', { name: 'View details' }).click();

3. Wait for the result of the click

Use a condition tied to the actual result instead of an arbitrary sleep. Common conditions include a dialog becoming visible, a menu gaining an expanded state, a URL changing, a loading indicator disappearing, or a result row appearing.

// Dialog appears
await page.getByRole('button', { name: 'Open details' }).click();
await expect(page.getByRole('dialog')).toBeVisible();

// A result appears after an asynchronous request
await page.getByRole('button', { name: 'Load more' }).click();
await expect(page.getByText('Item 21')).toBeVisible();

// A loading indicator disappears
await page.getByRole('button', { name: 'Refresh' }).click();
await expect(page.getByRole('status', { name: 'Loading' })).toBeHidden();

If you use assertions, install the Playwright test package or use an explicit locator wait in a plain script:

await page.getByRole('button', { name: 'Open details' }).click();
await page.getByRole('region', { name: 'Details' }).waitFor({ state: 'visible' });

Choose a condition that proves the state you want in the image. A fixed delay can be useful as a last resort for an animation with no observable state, but it can be too short on a slow run and unnecessarily slow on a fast one.

4. Handle clicks that navigate

When the click starts navigation, coordinate the navigation wait and the click. Waiting only after the click can create a race if navigation begins immediately:

const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle' }),
  page.getByRole('link', { name: 'Documentation' }).click(),
]);

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

Use domcontentloaded when you only need the new document parsed. Use a page-specific assertion after navigation when the screenshot depends on client-side rendering:

await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.getByRole('link', { name: 'Documentation' }).click(),
]);
await expect(page.getByRole('heading', { name: 'Documentation' })).toBeVisible();

Playwright’s click action performs actionability checks, scrolls the target into view, clicks it, and waits for initiated navigation unless configured otherwise. The resulting application state may still require its own assertion.

5. Capture the page or only the clicked result

Use a page screenshot when the whole page state matters:

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

Use a locator screenshot when you only need the matched component:

const details = page.getByRole('region', { name: 'Details' });
await details.screenshot({ path: 'details.png' });

A locator screenshot captures the matched region. If another element covers part of it, the covered pixels are not visible. For a scrollable container, the capture reflects the content currently visible in that container. See the locator screenshot API.

Useful page screenshot options include:

Option Use
path Output file name.
fullPage Capture the full scrollable page instead of the viewport.
type Choose PNG or JPEG.
quality Set JPEG quality; it does not apply to PNG.
omitBackground Keep transparency where supported.
animations Control animation handling in supported Playwright versions.

6. A Puppeteer equivalent

Puppeteer’s locator interaction also checks that an element is in the viewport, visible, enabled, and stable before clicking. Coordinate navigation and clicking with Promise.all:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  try {
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
    await page.locator('::-p-aria(Open details)').click();
    await page.locator('::-p-text(Details)').wait();
    await page.screenshot({ path: 'after-click.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

For a navigation click, follow Puppeteer’s documented coordination pattern:

await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle0' }),
  page.locator('::-p-aria(Documentation)').click(),
]);
await page.screenshot({ path: 'documentation.png' });

See Puppeteer’s Locator API for the current locator syntax and behavior.

7. Common failures and fixes

Symptom Likely cause Fix
“Locator resolved to more than one element” The locator is not specific enough. Add the accessible name, filter by surrounding text, or scope it to a region.
Timeout waiting to click The control is hidden, covered, disabled, or not yet rendered. Wait for the correct state, inspect the overlay, and use a locator tied to the visible control.
Screenshot shows the old state The click started an asynchronous update that was not awaited. Wait for the dialog, text, URL, network result, or other state that proves completion.
Navigation wait times out The click does not navigate, or the page uses client-side routing. Remove the navigation wait and assert the route or rendered content instead.
Only part of an element is captured A sticky header or overlay covers it, or the element is inside a scrolled container. Dismiss the overlay, scroll the container intentionally, or capture the page instead of the locator.
Click works locally but not in CI Different viewport, fonts, timing, permissions, or network conditions. Set the viewport, wait on a meaningful state, use deterministic test data, and record a trace or screenshot on failure.
Full-page image misses lazy content Images load only after scrolling or intersection. Scroll through the page or use a capture service that supports full-page lazy-image loading.

8. Reliability and performance checklist

  • Use a role, accessible name, label, text, or stable test ID before reaching for a structural selector.
  • Await every click.
  • Wait for the resulting state, not a guessed number of milliseconds.
  • Coordinate click and navigation waits with Promise.all.
  • Set a consistent viewport, color scheme, locale, timezone, and device scale when pixel consistency matters.
  • Disable or hide animations that make captures vary between runs.
  • Capture only the required scope; locator screenshots are smaller and faster than full-page images.
  • Use retries for transient network failures, but investigate repeated timeouts instead of masking them.
  • Keep browser instances reused within a batch while isolating pages or contexts when cookies and local storage must differ.

Browser automation gives you full control, but it also means you maintain browser binaries, wait logic, consent dialogs, popups, bot checks, and failed-load handling. Your infrastructure cost includes the browser runtime, network traffic, storage, and the engineering time needed to keep selectors and waits current.

9. Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. It supports clicking an element before capture, so you can configure the interaction and then request the resulting screenshot without maintaining Playwright or Puppeteer code. The API also supports full-page capture, element capture by CSS selector, custom JavaScript and CSS, waits, headers, cookies, user agents, blocking rules, device settings, PDFs, caching, bulk capture, signed links, and asynchronous jobs. See the ScreenshotNeo API documentation for the available options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 fs = require('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether it was billed. An MCP server lets Claude, Cursor, and other MCP clients take screenshots. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

10. Cost and output considerations

For self-hosted automation, reduce cost by reusing a browser, avoiding unnecessary full-page captures, and waiting on precise conditions. For an API, cache stable pages with a suitable TTL and request JPEG or WebP when a smaller file is acceptable. Treat a cache hit according to the provider’s billing rules; ScreenshotNeo identifies cache hits in its response and does not bill them.

FAQ

Should I click by text or by CSS selector?

Prefer a user-facing role, accessible name, label, or text. Use a stable CSS attribute when the page does not expose a reliable user-facing locator.

Do I need to wait after every click?

Await the click itself. Add a second wait when the click triggers navigation, animation, network work, or client-side rendering that affects the screenshot.

How do I screenshot only the opened panel?

Locate the panel after the click and call its locator screenshot method. Capture the page instead if overlays or clipping make the component incomplete.

Why does a click succeed but the screenshot remain unchanged?

The click may have triggered an asynchronous update, been intercepted by an overlay, or targeted a different matching element. Assert the expected visible result before capturing.