How to Scroll to the Top of a Page with Playwright
Use Playwright to reset the document or a nested scroll container to the top, with deterministic code, alternatives, troubleshooting, and screenshot tips.
Direct answer: to move the whole document to the top, run await page.evaluate(() => window.scrollTo(0, 0));. To reset a nested scrollable element, set that element’s scrollTop to zero with locator.evaluate().
await page.evaluate(() => window.scrollTo(0, 0));
For a scrollable panel, menu, feed, or other element:
const panel = page.getByTestId('scrolling-container');
await panel.evaluate(element => {
element.scrollTop = 0;
});
Playwright automatically scrolls most locators into view before actions, so add an explicit reset only when the exact scroll position matters. See the official scrolling guide, Locator API, and Page API.
Choose the right scrolling method
| Goal | Method | Why |
|---|---|---|
| Put the document viewport at coordinate zero | page.evaluate(() => window.scrollTo(0, 0)) |
Sets the page position directly and deterministically. |
| Reset an inner scroll area | locator.evaluate(element => element.scrollTop = 0) |
Changes the selected element’s own scroll position. |
| Expose a known target | locator.scrollIntoViewIfNeeded() |
Scrolls only when the target is not completely visible. |
| Reproduce user wheel input | page.mouse.wheel(0, amount) |
Models a wheel gesture, but does not guarantee an exact final position. |
Set up a complete Playwright example
The following TypeScript program opens a page, returns the document to the top, verifies the position, and closes the browser.
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => window.scrollTo(0, 0));
const position = await page.evaluate(() => ({
x: window.scrollX,
y: window.scrollY
}));
console.log(position); // { x: 0, y: 0 }
await browser.close();
})();
Install Playwright with npm i -D playwright. If your project uses JavaScript, remove the TypeScript-only type annotations; this example has none, so it runs unchanged as a .js file in a project configured for ESM.
Scroll the whole document to the top
Instant positioning
await page.evaluate(() => window.scrollTo(0, 0));
page.evaluate() executes its callback in the browser page context. The callback calls the standard browser window.scrollTo operation, with horizontal and vertical coordinates both set to zero.
Verify the result
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForFunction(() => window.scrollY === 0);
const y = await page.evaluate(() => window.scrollY);
if (y !== 0) {
throw new Error(`Expected scrollY to be 0, received ${y}`);
}
Verification is useful when application code immediately restores a previous position, a scroll handler moves the page, or a browser extension changes scrolling behavior.
Animated scrolling
For a screenshot or assertion that needs a stable exact position, use the default instant behavior. If you specifically need to exercise an animated interaction, request smooth behavior in the page context:
await page.evaluate(() => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
});
await page.waitForFunction(() => window.scrollY === 0);
Smooth scrolling is asynchronous. Waiting for the position avoids capturing an intermediate frame.
Reset a nested scrollable element
A page can be at the top while an inner element remains scrolled. Select the element that owns the scroll bar and set its scrollTop:
const panel = page.getByTestId('scrolling-container');
await panel.evaluate(element => {
element.scrollTop = 0;
});
You can use another reliable locator when a test ID is unavailable:
const feed = page.locator('[aria-label="Results"]');
await feed.evaluate(element => {
element.scrollTop = 0;
element.scrollLeft = 0;
});
Confirm that the element is actually scrollable before treating a zero value as a meaningful reset:
const state = await feed.evaluate(element => ({
top: element.scrollTop,
height: element.scrollHeight,
clientHeight: element.clientHeight,
canScroll: element.scrollHeight > element.clientHeight
}));
console.log(state);
If the container is inside an iframe, first select the frame and then locate the element within it:
const frame = page.frameLocator('iframe[title="Results"]');
await frame.locator('[data-testid="scrolling-container"]').evaluate(element => {
element.scrollTop = 0;
});
Bring an element into view instead
When the real requirement is “make this heading or button visible,” do not reset the entire document. Use scrollIntoViewIfNeeded():
const heading = page.getByRole('heading', { name: 'Page title' });
await heading.scrollIntoViewIfNeeded();
await expect(heading).toBeVisible();
The locator method waits for actionability checks and scrolls only when the element is not completely visible. This is usually the clearest choice for a click or assertion whose target is known.
Simulate a real mouse wheel
Use wheel input when the behavior under test depends on user scrolling, lazy loading, or an infinite list:
const panel = page.getByTestId('scrolling-container');
await panel.hover();
await page.mouse.wheel(0, -500);
Wheel amounts are relative and browser-dependent. They are appropriate for interaction tests, but a direct scrollTop = 0 or window.scrollTo(0, 0) call is more deterministic when the final position must be exactly the top.
Use scrolling before screenshots
Reset the page before capturing a viewport screenshot when the requested image must begin at the top:
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForFunction(() => window.scrollY === 0);
await page.screenshot({ path: 'top.png' });
For a full-page screenshot, Playwright lays out the complete document, so resetting the viewport is usually unnecessary. It can still help when your page has scroll-position-dependent headers, lazy components, or scripts that change content after the first scroll.
Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
window is not defined |
The code ran in the Node.js test process instead of the page context. | Put the call inside page.evaluate(() => ...). |
| The document stays scrolled | A script restores the previous position or moves it after your call. | Run the reset after the navigation and wait for window.scrollY === 0; inspect scroll handlers if it moves again. |
| The panel stays scrolled | You reset window, but the scrollbar belongs to a nested element. |
Locate that element and set its scrollTop to zero. |
locator.evaluate times out |
The locator matches no element, or the element is not attached. | Use a stable test ID or role, wait for the component, and check the locator count. |
| Wheel input moves the wrong area | The pointer is not over the intended scroll container. | Call locator.hover() first, or use direct position assignment. |
| The screenshot captures a middle position | Smooth scrolling or a late scroll handler is still active. | Use instant scrolling and wait until the measured position is zero before capture. |
| Clicks fail even though the page looks near the top | A sticky header covers the target. | Scroll the target with scrollIntoViewIfNeeded(), then account for the fixed header in the test or use a more specific assertion. |
Reliability and performance checklist
- Prefer stable locators such as test IDs, roles, or labels for nested containers.
- Use direct position changes for deterministic setup; reserve wheel input for interaction coverage.
- Wait for the page state you need before scrolling: navigation completion, component rendering, or network activity.
- Measure
window.scrollYor the element’sscrollTopwhen a later step depends on the reset. - Avoid repeated evaluate calls in a tight loop; set the position once and then perform the dependent action.
- For infinite lists, scroll in increments and wait for new content rather than assuming one large wheel event loads everything.
- Keep viewport dimensions and device scale consistent when screenshots are compared in CI.
Or skip the browser setup
If your goal is a clean screenshot rather than testing scroll behavior, ScreenshotNeo returns an image or PDF from one API request. Its capture options include full-page screenshots with lazy images loaded, custom viewport and device presets, retina scale, waits, custom JavaScript, and element capture.
cURL (see the ScreenshotNeo API docs):
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}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed; the response identifies the page verdict and billing status in headers. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can take screenshots, inspect pages, and capture PDFs. The Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000 shots.
Create your free ScreenshotNeo account and start with 1,000 screenshots per month at no charge.
FAQ
Should I scroll before every Playwright action?
No. Playwright automatically scrolls most actionable locators into view. Add an explicit reset when your assertion, screenshot, or application behavior depends on a known position.
What is the difference between scrollTop and window.scrollY?
window.scrollY reports the document viewport’s vertical offset. scrollTop reports an individual scrollable element’s offset. Reset the property owned by the scrollbar you need to move.
Can I use this with Java?
Yes. The Java binding exposes the same concepts through its locator evaluation and mouse APIs. Follow the official Java scrolling guide for binding-specific syntax.
Why does a wheel event not reach exactly zero?
A wheel event is a relative input, and the resulting distance depends on browser and page behavior. Use a direct scroll-position assignment when zero must be exact.
How do I scroll a shadow-DOM component?
Locate the exposed component or scroll host, then evaluate its scrollTop. If the component does not expose a stable selector, add one in the application or test fixture rather than relying on generated DOM structure.


