How to Capture a Logged-In Webpage Screenshot with Playwright and Saved Cookies
Save Playwright authentication state after sign-in, restore it in a browser context, verify the session, and capture the page safely.
To capture a logged-in page with Playwright, sign in once, save the browser context’s storage state to a private JSON file, then create a new context with that file and navigate to the page. Wait for a site-specific sign-in indicator before saving the screenshot. Playwright storage state includes cookies and local storage, and can include IndexedDB; it does not automatically preserve session storage.
This guide uses TypeScript with Playwright. The same workflow applies to JavaScript by removing the type-specific syntax in the optional session-storage example.
1. Install Playwright and keep authentication state private
npm init -y
npm install -D playwright
npx playwright install chromium
mkdir -p playwright/.auth
Add the state directory to .gitignore before saving credentials:
printf '\nplaywright/.auth/\n' >> .gitignore
The state file can contain session cookies or tokens that allow access to the account. Treat it like a password: do not commit it, publish it, attach it to bug reports, or store it in a shared artifact without access controls. Use a test account with the minimum access needed.
2. Sign in and save the storage state
Create save-auth.ts. Replace the example URL, selectors, and credentials with the target site’s login flow. Prefer environment variables for credentials rather than putting them in source code.
import { chromium } from 'playwright';
const email = process.env.TEST_EMAIL;
const password = process.env.TEST_PASSWORD;
if (!email || !password) {
throw new Error('Set TEST_EMAIL and TEST_PASSWORD before running this script.');
}
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
// Use a reliable condition for this site. A click alone does not prove
// that the authentication cookies and tokens have been established.
await page.waitForURL('https://example.com/account**');
await page.getByRole('button', { name: 'Account menu' }).waitFor({ state: 'visible' });
await context.storageState({ path: 'playwright/.auth/user.json' });
console.log('Saved authenticated browser state.');
} finally {
await browser.close();
}
Run it with credentials supplied by your shell or CI secret store:
TEST_EMAIL='you@example.com' TEST_PASSWORD='replace-me' npx tsx save-auth.ts
If the project does not already have tsx, install it with npm install -D tsx. You can also save the state from an existing Playwright Test setup project after its login flow has reached a verified destination. The essential save call is await page.context().storageState({ path: 'playwright/.auth/user.json' }).
Some login pages redirect through several URLs, use a one-time code, or update the page without changing its URL. In those cases, wait for an authenticated UI element or another site-specific signal instead of relying on the example URL. If a login requires human interaction, complete that flow in the browser and save state only after verifying that the account is signed in.
3. Restore the state and capture the page
Create capture.ts. The readiness check should identify the actual signed-in page and, when relevant, the content you intend to capture.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
viewport: { width: 1440, height: 1000 },
});
const page = await context.newPage();
await page.goto('https://example.com/account', { waitUntil: 'domcontentloaded' });
// Replace with a reliable element that only appears when signed in.
await page.getByRole('button', { name: 'Account menu' }).waitFor({ state: 'visible' });
await page.getByRole('heading', { name: 'Account overview' }).waitFor({ state: 'visible' });
await page.screenshot({ path: 'account.png' });
} finally {
await browser.close();
}
Run the capture with npx tsx capture.ts after installing tsx as shown above. The restored state only works while the website accepts it; expired or revoked sessions need a fresh login and state file.
4. Choose the screenshot area and format
| Goal | Playwright call | Notes |
|---|---|---|
| Current viewport | await page.screenshot({ path: 'view.png' }) |
Captures the visible browser viewport. |
| Full scrollable page | await page.screenshot({ path: 'full.png', fullPage: true }) |
Captures the full page height. Long pages can produce large images and may trigger lazy loading behavior that varies by site. |
| One component | await page.locator('.account-panel').screenshot({ path: 'panel.png' }) |
Wait for the locator to be visible and unique enough to identify the intended element. |
| Image bytes in memory | const png = await page.screenshot() |
Returns a buffer when no path is specified; useful for uploading or further processing. |
Screenshot options include type ('png', 'jpeg'), JPEG quality, fullPage, clip, omitBackground, animations, caret, and scale. Use PNG for crisp UI and text; JPEG quality applies to JPEG output. A clip rectangle captures a specific viewport region. omitBackground: true can make supported PNG captures transparent. Disabling animations can make visual captures more stable; hiding the caret avoids a blinking text cursor in form screenshots. Playwright’s screenshot API documents the supported options and constraints for the installed version.
await page.screenshot({
path: 'account-full.png',
type: 'png',
fullPage: true,
animations: 'disabled',
caret: 'hide',
scale: 'css',
});
Use scale: 'css' when you want output dimensions to follow CSS pixels rather than device pixels. For a particular component, capture its locator directly rather than guessing a clip rectangle.
5. Understand what Playwright saves
| Authentication mechanism | Included in storage state? | What to do |
|---|---|---|
| Cookies | Yes | Restore with storageState when creating the context. |
| Local storage | Yes | Restore the saved context state; confirm the app still accepts the saved token. |
| IndexedDB | Optional | On Playwright v1.51 or newer, save with indexedDB: true if the app stores authentication data there. |
| Session storage | No built-in persistence in the authentication guide | Save and restore it separately with an init script, before the application code runs. |
| Virtual WebAuthn credentials | Optional, version-dependent | The BrowserContext API documents a credentials option added in v1.61. Check the installed version and target flow. |
For IndexedDB-backed authentication, use the version-supported option when saving:
await context.storageState({
path: 'playwright/.auth/user.json',
indexedDB: true,
});
Do not assume every application’s sign-in token is in a cookie. If a restored context returns to the login page, check the site’s storage mechanism and whether the session has expired.
Session storage example
Session storage is scoped to an origin and is not included by storageState. If the application depends on it, capture only the required values after sign-in, keep them secret, then seed the origin before navigation. This example is a pattern; adapt it to the site’s keys and avoid dumping tokens into logs.
// During the authenticated session, collect only the keys the app needs.
const sessionValues = await page.evaluate(() => ({
authToken: sessionStorage.getItem('authToken'),
}));
// Store sessionValues in a protected file or secret store, not source control.
// In the capture script, before navigating to the target page:
await context.addInitScript(({ values }) => {
if (location.origin === 'https://example.com') {
for (const [key, value] of Object.entries(values)) {
if (typeof value === 'string') sessionStorage.setItem(key, value);
}
}
}, { values: sessionValues });
await page.goto('https://example.com/account');
The initialization script must run before the site’s code reads session storage. Keep the origin check so those values are not written to unrelated sites. Playwright documents that it does not provide a built-in API to persist session storage.
6. Use saved state in Playwright Test
For a test suite, Playwright recommends a setup project that authenticates and saves state, with dependent test projects loading that state. A minimal configuration can look like this:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Put the login flow in the setup test and write the state only after its authenticated condition passes. Playwright Test can use the configured state for pages created by that project. If parallel tests change server-side data, use a separate account per worker; a shared account is appropriate only when tests do not interfere with one another.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The capture shows the login page | State was saved too early, the session expired, or authentication uses storage not in the snapshot. | Verify sign-in before saving; refresh state; inspect whether the app needs IndexedDB or session storage. |
| Login redirect is still running when state is saved | The script waited only for a button click or a generic load event. | Wait for the final URL or a signed-in-only element before calling storageState. |
| Cookie exists but is rejected | It may be expired, scoped to another domain/path, or tied to browser/session conditions. | Recreate state through the site’s normal login flow and use the correct origin. |
| “Timeout exceeded” while waiting for an element | The selector is wrong, the page is not ready, or the session is not authenticated. | Check the actual DOM and URL, use a site-specific locator, and distinguish login failure from slow rendering. |
| Blank or incomplete screenshot | Capture happened before the relevant content rendered, or the app shows an error/empty state. | Wait for a meaningful content locator; handle loading and empty states explicitly before capture. |
| “Browser executable doesn’t exist” | The Playwright package is installed but its browser binary is missing. | Run npx playwright install chromium in the environment that runs the script. |
| State file appears in Git changes | The auth directory was not ignored or the file was already tracked. | Add playwright/.auth/ to .gitignore; if already tracked, remove it from the index and rotate affected credentials. |
| Concurrent tests sign each other out or change data | Workers share one account whose server-side state is mutable. | Use isolated accounts per worker or serialize tests that mutate shared state. |
8. Performance, reliability, and cost
Reusing saved state avoids repeating the interactive login flow for every capture, but the destination page still has to load and render. Keep browser instances and contexts scoped sensibly: reuse a browser for multiple captures where appropriate, but create separate contexts when sessions or viewport settings must be isolated. Close contexts and browsers in a finally block so failures do not leave processes running.
Reliability depends on the site’s session lifetime, anti-automation controls, network access, and readiness signals. A successful navigation event alone does not prove that the page is signed in or visually complete. Verify the authenticated UI and the content that matters. Refresh saved state when the site expires it, and avoid sharing one mutable account across parallel workers.
Playwright itself is an open-source browser automation library; this workflow’s practical costs are the machine time and infrastructure used to run a browser, plus any account or service requirements of the site being captured. Large full-page images, repeated browser launches, and slow pages increase processing time and storage or transfer needs. The research sources provide no benchmark figures, so performance should be measured on the target site and runtime.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. For a public page, one GET request returns an image or PDF. Its clean-shot flow accepts cookie or consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page info, and PDF capture.
This is an API alternative for pages it can access; it does not take your Playwright saved-cookie file as authentication state. Do not send private session cookies or credentials to an API unless its documented authentication and data handling meet your requirements.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. Python:
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)
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 import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; yearly billing gives two months free, and every feature is on every plan. Sign up for 1,000 free screenshots a month, no card required.
FAQ
Can I use a saved state file on another machine?
Yes, if you transfer it through a secure channel and the website still accepts the session. The file is sensitive and may stop working after expiry, revocation, or environment changes.
Does storage state include passwords?
It snapshots browser storage such as cookies and local storage, not the password you typed into the login form. Treat the resulting tokens as credentials because they may grant account access.
Can I take a screenshot without writing it to disk?
Yes. Omit the path option and page.screenshot() returns image bytes that your script can pass to another process or storage service.
Can Playwright guarantee a site will allow an automated signed-in session?
No universal guarantee applies. Sites can expire sessions or apply authentication and automation controls; use the site’s permitted login flow and refresh state when needed.


