How to Capture Screenshots of a React App with an AI Agent After Hydration
Wait for the React state your screenshot needs, then capture it with an AI agent using Playwright, a stable readiness cue, and repeatable settings.
To capture a React app after hydration, have the AI agent wait for an application-specific signal that the exact content and state you want are ready, then take the screenshot. Visible server-rendered HTML alone does not prove that React has hydrated or that asynchronous data has loaded. A stable marker such as data-app-ready="true" makes readiness explicit; set it only when the page is ready for the capture.
This guide uses Playwright with Node.js. It covers full-page and element captures, a Python alternative, repeatability, common failures, and an API option when you do not want to manage a browser.
1. Decide what “ready” means
React hydration attaches client behavior to HTML that may already have been rendered by the server. That means the page can look populated before the client-side behavior is attached. React also expects the first client render to match the server-rendered content, and warns that calling root.render before hydration finishes can clear the server HTML and switch to client rendering. The React documentation describes hydration as turning the server’s initial HTML snapshot into a fully interactive browser app: React hydrateRoot.
There is no universal browser-observable “hydration complete” event established by the React documentation reviewed here. Treat readiness as an application contract: decide what the screenshot must show, then expose a signal tied to that state. If the page loads data after mounting, the signal should not become ready until that data is available too.
| Capture goal | Useful readiness condition |
|---|---|
| Dashboard with account data | Data request completed and the expected dashboard state rendered |
| Search results | Results are visible, including an explicit empty state when there are no matches |
| Interactive control | The control is rendered and its required client behavior is attached |
| Static server-rendered page | A stable landmark or expected content is visible, if no client state is needed |
2. Add a page-specific readiness marker
Set the marker from the state the screenshot depends on. This example assumes dataReady becomes true only after the required application data has loaded and the view is ready to show.
function AppView({ dataReady, children }) {
return (
<main data-app-ready={dataReady ? "true" : "false"}>
{children}
</main>
);
}
Do not set the marker unconditionally just because the component mounted. For a page with multiple independent requirements, derive readiness from all of them, for example: required data loaded, loading indicator gone, and the target panel rendered. Keep the marker stable long enough for the automation to observe it.
If you cannot change the application, wait for a meaningful visible landmark or expected content instead. Choose something that distinguishes the intended state from the initial server HTML or loading view. A generic selector such as body usually proves too little.
3. Capture with an AI agent using Playwright
An AI agent can run the following Node.js script as a browser automation step. It navigates to the app, waits up to 15 seconds for the app-owned marker, saves a full-page image, and closes the browser even if navigation or capture fails.
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
});
await page.goto('http://localhost:3000', {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
await page.locator('[data-app-ready="true"]').waitFor({
state: 'visible',
timeout: 15000,
});
await page.screenshot({
path: 'react-app.png',
fullPage: true,
animations: 'disabled',
});
console.log('Saved screenshot to react-app.png');
} finally {
await browser.close();
}
Install Playwright in the project and install its browser before running the script. The script uses ES modules; save it with an .mjs extension or configure the project for ES modules. Check your installed Playwright version for support of screenshot options, since its API adds options over time. See the Playwright Page API.
domcontentloaded is only a navigation milestone here, not the readiness condition. The explicit locator wait is what connects the capture to the application state. Playwright recommends locator-based waits and web-first assertions over the discouraged page.waitForSelector() API.
Capture just one component
For a component or region, take a locator screenshot rather than a whole-page image:
const panel = page.locator('[data-testid="report-panel"]');
await panel.waitFor({ state: 'visible', timeout: 15000 });
await panel.screenshot({ path: 'report-panel.png', animations: 'disabled' });
Locator screenshots scroll the target into view and capture its bounds. They can fail if the element detaches during capture; use a stable locator and make sure the app is not replacing the target during the wait. See the Playwright Locator API.
Wait for expected content when you cannot add a marker
If the page has a stable landmark that reliably represents the intended state, wait for that locator:
const heading = page.getByRole('heading', { name: 'Monthly report' });
await heading.waitFor({ state: 'visible', timeout: 15000 });
await page.screenshot({ path: 'report.png', fullPage: true });
For content that can legitimately be absent, wait for a state that covers both outcomes, such as either results or an explicit “no results” message. Avoid treating a fixed delay as proof of readiness: network and rendering time vary, and a delay can be either unnecessarily slow or too short.
4. Make captures repeatable
- Use a fixed viewport and browser. Layout and rendering affect pixels. For visual comparisons, keep browser and operating-system conditions consistent or maintain separate baselines for different environments.
- Disable animations where appropriate. Playwright supports animation controls for screenshots. This reduces captures taken mid-transition.
- Control hover state. Screenshots include hover effects present at capture time. Move the mouse away from interactive elements if hover styling is not part of the intended image.
- Handle volatile content deliberately. Timestamps, rotating content, and live counters can change between runs. Use test data or a screenshot stylesheet to hide content that is outside the test’s purpose.
- Choose the correct scope. Use a regular page screenshot for the viewport,
fullPage: truefor the scrollable page, or a locator screenshot for a specific region.
Playwright Test can capture and compare screenshot baselines; the first run can create a reference image and later runs compare against it. Rendering can vary across fonts, browsers, and platforms, so use a consistent environment. See Playwright visual comparisons.
5. Troubleshoot readiness and capture failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot shows the loading or server-rendered state | The wait condition only proves that navigation or initial HTML arrived | Wait for an app-owned marker derived from the required data and UI state |
| Readiness wait times out | The marker never becomes true, is not visible, or the page failed before reaching the intended state | Inspect the app’s loading and error states; verify the selector and readiness logic; keep the timeout bounded so failure is diagnostic |
| Screenshot succeeds but content is incomplete | The marker is set at mount rather than after the required asynchronous work | Move the readiness condition to the point where all screenshot-required content is rendered |
| Element screenshot reports a detached element | The app replaced or rerendered the target during capture | Wait for a stable state, locate the element again after transitions, and avoid selectors tied to transient nodes |
| Images or lower-page content are missing | The capture happened before lazy content loaded, or only the viewport was captured | Use full-page capture when needed and ensure the application’s readiness contract accounts for content required in the image |
| Snapshots differ between machines | Browser, operating system, font availability, viewport, animations, hover, or dynamic content differ | Pin the capture environment and viewport; control animation and volatile elements; use environment-specific baselines when necessary |
| Screenshot options are rejected | The installed Playwright version does not support an option used by the script | Check the installed version’s API documentation and use options available in that version |
6. Cost, speed, and reliability considerations
For a local or test environment, Playwright keeps capture close to the app and gives the agent control over navigation, readiness waits, and screenshot scope. The main reliability choice is the readiness contract: a useful timeout should fail with evidence when the page is broken, rather than silently producing an early image. A fixed delay is simple but makes every capture wait and still does not guarantee the intended state.
Full-page captures and complex pages can take longer and produce larger files than a small locator capture. Capture only the scope required, use a fixed viewport, and keep browser setup consistent for repeatable visual comparisons. The research sources do not provide benchmark or cost figures for Playwright, so none are asserted here.
Or skip the browser setup
If you need a screenshot of a public React page without managing a browser, ScreenshotNeo provides a website screenshot API and MCP server. A screenshot API cannot know your app’s private readiness contract, so use this for pages where the requested state is available through the URL and normal page loading.
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
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 request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Replace the example URL with the page you want to capture. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does React provide a “hydration complete” event for Playwright?
The reviewed React documentation does not establish a universal browser-observable event for automation. Use a page-specific readiness marker or a reliable visible landmark tied to the state you need.
Is networkidle enough to know the app is ready?
Network quiet does not by itself prove that the intended application state is rendered. Use an application-level condition that represents the content in the screenshot.
Should I capture the entire page or a locator?
Capture the full page when the whole scrollable document matters. Use a locator screenshot when only one component or region is needed.
Why does the page look ready but a control is not interactive?
Visible HTML can precede client hydration. Wait for a marker tied to the required interactive state rather than relying on visual presence alone.


