How to Use Percy with Next.js Pages That Need JavaScript Rendering
Use Playwright to let Next.js render the page, wait for the state you want to test, then send its DOM to Percy. Learn how capture timing, Percy’s renderer, and responsive widths affect your snapshots.
Short answer: run your Next.js app, open the route in Playwright, wait until the JavaScript-rendered UI reaches the state you want to protect, then call Percy’s Playwright snapshot function. The test browser runs your app’s JavaScript and Percy captures the resulting DOM. Percy later renders that captured DOM in its own environment, where JavaScript is disabled by default.
That distinction matters: Percy does not need to rerun your Next.js application code to capture a client-rendered state. Its general Playwright integration works with Next.js, but BrowserStack’s documentation does not describe a special Next.js mode. This guide uses the regular @percy/playwright integration and a project’s existing Playwright setup. See BrowserStack’s Playwright integration guide and SDK capture workflow.
1. Understand where JavaScript runs
There are two separate browser stages:
- Test browser: Playwright navigates to your running Next.js app. JavaScript executes normally, including React hydration, client-side data fetching, and event-driven UI updates. The Percy SDK serializes the DOM state that exists when you call the snapshot function.
- Percy renderer: Percy discovers the assets used by that DOM, then renders the captured snapshot for comparison. JavaScript is disabled here by default because the captured DOM already reflects the app’s JavaScript changes.
So a page that depends on JavaScript can still be captured. The important requirement is that the intended state has appeared before the snapshot call. Enabling JavaScript in Percy’s separate renderer is a different setting; it is not required just because the Next.js page uses JavaScript. Percy documents that enabling JavaScript in the renderer can cause side effects such as redirects, animation, or loss of serialized state.
2. Add Percy to an existing Next.js and Playwright project
This example uses TypeScript, Playwright Test, and the Percy Web project path. It assumes the project already has Next.js and Playwright installed and a route at /dashboard that displays an element with data-testid="dashboard-ready" when its client-rendered content is ready.
- Create a Percy Web project and copy its project token from Percy.
- Install the Percy CLI and Playwright SDK as development dependencies:
npm install --save-dev @percy/cli @percy/playwright
Keep the token in an environment variable named PERCY_TOKEN; do not commit it to source control. For a local macOS or Linux shell:
export PERCY_TOKEN="your-project-token"
For Windows PowerShell:
$Env:PERCY_TOKEN="your-project-token"
Add or adapt a Playwright test such as tests/percy-dashboard.spec.ts:
import { test, expect } from '@playwright/test';
import percySnapshot from '@percy/playwright';
test('dashboard visual state', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('http://127.0.0.1:3000/dashboard');
// This should identify the user-visible state that must be present.
await expect(page.getByTestId('dashboard-ready')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await percySnapshot(page, 'Dashboard – loaded');
});
Use a stable, unique snapshot name that says what the captured state represents. The readiness assertions above are examples; replace them with selectors and conditions that match your app’s actual content and state.
Run the app and test suite. For example, if the project’s Playwright command is npx playwright test and its app start command is npm run start after a production build, the sequence is:
npm run build
npm run start
In another terminal, run the existing browser tests under Percy:
npx percy exec -- npx playwright test
Many Playwright projects already start a development server using the webServer setting in playwright.config.ts. You can keep that setup and wrap the normal Playwright test command with percy exec. If you add a script, adapt the command to your project’s scripts, for example:
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:percy": "percy exec -- playwright test"
}
}
Then run npm run test:e2e:percy in CI with PERCY_TOKEN configured as a secret. The server start command, test command, readiness checks, and CI configuration depend on your project. Percy’s integration guide documents the SDK, token, snapshot call, and percy exec workflow.
3. Wait for the right JavaScript-rendered state
A successful navigation is not proof that the state you care about is ready. Next.js can render an initial shell, hydrate it, and then update content after a client-side request. Capture after the particular visible state under test has appeared and stabilized.
- Prefer a meaningful selector: wait for the result list, user name, chart, or status element whose appearance proves readiness.
- Assert important content: use Playwright assertions such as
toBeVisible()ortoHaveText()before capturing. - Use a test-controlled state where possible: seed predictable data or mock the app’s API response using your existing test setup.
- Use a delay only for a real timing requirement: fixed sleeps can make the suite slower and remain unreliable when load times vary.
networkidle is not universally the correct readiness signal. Applications with polling, analytics, streaming, or other ongoing requests may never become idle, while a page can become network-idle before a deferred UI update is visible. Choose the condition that proves the page has reached the state you want to compare.
4. Configure responsive widths and snapshot behavior
Percy’s responsive widths control the viewport sizes used when Percy renders a captured DOM. The documented default widths are 375 and 1280 pixels; you can set project-wide widths in Percy configuration. Each width creates a separate screenshot for monthly usage accounting, so include widths that protect meaningful layout breakpoints.
version: 2
snapshot:
widths: [375, 768, 1280]
min-height: 900
enable-javascript: false
This is an example .percy.yml. Choose widths that match the layouts your team needs to cover. BrowserStack documents responsive width configuration and the usage implication in its responsive testing guide.
| Choice | When it fits | Tradeoff |
|---|---|---|
| One browser and a few widths | You mainly need to protect your own supported desktop and mobile layouts. | Less coverage of browser-specific rendering differences. |
| Percy Web browser selection | You want Percy’s browser selection controls for visual rendering. | Browser selection is managed in Percy’s project settings. |
| Percy with Automate | You need to control browser and platform combinations through Automate. | Browser selection and capabilities are configured for the Automate session. |
Percy offers both Percy Web and Percy with Automate; where browser selection is controlled depends on the selected path. Review BrowserStack’s project setup choices before creating the project, especially if you require specific browser and platform coverage.
If the DOM itself changes with viewport size because your app’s JavaScript responds to resize events, consider Percy’s responsive DOM snapshot guidance. In that case, the test browser may need to be resized and the DOM captured at each width; merely rendering one captured DOM at several widths may not reproduce JavaScript-driven DOM changes. See responsive DOM snapshots.
Other useful configuration points include:
enable-javascript: controls JavaScript in Percy’s renderer, not whether JavaScript ran in the Playwright test browser. Its default is false. Leave it off unless you have a specific reason and have checked for renderer side effects.percy-css: apply Percy-specific CSS to stabilize or adjust a snapshot.scope: limit a snapshot to a selected part of the page when that is the intended comparison.min-height: control the minimum viewport height used for rendered snapshots.discovery: configure asset discovery, including additional allowed hostnames and authentication details such as request headers, authorization, or cookies.
Consult the current Percy configuration reference for supported keys and SDK-specific syntax.
5. Make assets and dynamic content reproducible
Percy captures the current DOM and discovers the assets needed to render it. Assets served from another host, behind authentication, or from a short-lived URL may need extra discovery configuration. A screenshot can contain the right DOM but still render incorrectly if Percy cannot retrieve a stylesheet, font, image, or API-backed asset.
- Make test data deterministic so a fresh run does not change names, timestamps, counts, or random values.
- Disable or freeze animations and transitions for visual snapshots where motion is not under test. Percy’s renderer applies some stabilization, and Percy-specific CSS can address remaining cases.
- For authenticated assets, configure the appropriate discovery headers, authorization, or cookies. Do not assume browser cookies from the Playwright session automatically authenticate Percy’s separate rendering requests.
- For third-party assets, check that the host is allowed for discovery and that the asset can be fetched in the Percy rendering environment.
6. Review baselines and visual differences
After the wrapped test run, open the Percy build and review the captured snapshots and their diffs. Approve intentional visual changes so subsequent builds compare against the intended design. The previous build is the default comparison baseline in the documented integration flow; baseline selection can be configured for workflows that need a different comparison build. See the Playwright integration documentation.
Review the same state, data, and widths consistently between builds. Otherwise, differences may reflect changed content or capture timing rather than a UI regression. Keep snapshot names stable across runs, and avoid duplicate names in a run unless you are using the documented responsive DOM capture workflow for merging widths.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshot shows a loading shell or missing client content | The snapshot call ran before hydration or async content finished. | Wait for and assert a visible element tied to the required state before calling percySnapshot. |
| The test passes locally but Percy shows missing images, fonts, or styles | Percy’s asset discovery could not retrieve an asset, often because it is hosted elsewhere or needs authentication. | Check the asset URL and access from the rendering environment; configure allowed hostnames and discovery authentication as needed. |
| Inputs or UI state look reset in Percy | JavaScript was enabled in Percy’s separate renderer and changed or cleared the already captured state. | Keep renderer JavaScript disabled unless necessary. Remember the test browser still runs JavaScript before capture. |
| Percy renders a redirect, animation, or different state | Renderer JavaScript was enabled, or the page’s state is dynamic. | Disable renderer JavaScript when possible, stabilize app data and animations, and capture after the target state is established. |
| Playwright waits forever for network idle | The app has polling, analytics, streaming, or other continuing requests. | Replace network-idle waiting with an assertion for the page state under test. |
| No Percy build or snapshots appear | The test was not wrapped with the Percy CLI, the token is unset or invalid, or the test command did not execute the intended tests. | Run npx percy exec -- npx playwright test, confirm PERCY_TOKEN is present in the process environment, and inspect command output. |
| Snapshot names collide or responsive snapshots overwrite one another | Names are duplicated in a run, or multiple viewport DOM states need the responsive capture workflow. | Use unique names for separate states. For responsive DOM snapshots, follow Percy’s documented defer-upload and viewport capture configuration. |
| Mobile and desktop results do not match expected layouts | The selected widths do not reflect the app’s breakpoints, or the DOM changes after viewport resize. | Choose representative breakpoints and use responsive DOM capture if client JavaScript changes the DOM by width. |
| Diffs change from run to run | Data, timestamps, animation, external content, or timing varies between runs. | Control test data, wait for an explicit stable state, and freeze or remove irrelevant motion and dynamic regions. |
8. Performance, reliability, and usage
The Playwright test must still launch the app, load the route, execute JavaScript, and wait for your chosen state. Percy then handles asset discovery, rendering, and comparison outside the test browser. Keep the wait condition precise: a selector-based state assertion is usually more reliable and faster than a long fixed delay or waiting for every network connection to stop.
Every configured responsive width counts as a separate screenshot toward Percy’s monthly screenshot usage. Add widths only when they correspond to layouts you need to protect. Cross-browser coverage also expands the set of rendering combinations you review; choose it when browser-specific behavior is part of your requirement. For details, see the official responsive usage notes and project type comparison.
Or skip the browser setup
If you need an image or PDF of a JavaScript-rendered URL without wiring a browser test, ScreenshotNeo provides a website screenshot API and MCP server. Its API opens the page in a browser and returns an image or PDF; the Percy workflow above remains useful when you need approved visual baselines and diffs.
See the ScreenshotNeo API documentation. cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
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', await res.arrayBuffer());
With Node.js on a runtime without Bun, write the returned bytes using that runtime’s filesystem API. ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed; its MCP server lets AI agents take screenshots; and 1,000 screenshots per month are free with no card, with paid plans starting at $5 for 3,000. Sign up for 1,000 free screenshots a month with no card.
FAQ
Does Percy have a Next.js-specific integration?
The cited setup is Percy’s general Playwright integration. Use it with a running Next.js app and adapt the route and readiness condition to the project.
Does the Percy renderer need JavaScript enabled to capture a client-rendered React component?
No. The test browser runs JavaScript before capture; Percy receives the resulting DOM state. Renderer JavaScript is a separate setting.
Can a test use a local Next.js server?
Yes. Start the app in the test environment and have Playwright navigate to its reachable address. The app start and readiness commands depend on your project and CI environment.
Should I compare every route at every width?
Use coverage that represents the layouts and states your team intends to protect. Every responsive width contributes a separate screenshot to usage.


