ScreenshotNeo

BlogHow-to

How to Take a Screenshot of a Logged-In Web App in an iframe

Use Playwright to authenticate in a browser context, wait for the embedded app to be ready, then capture the iframe, a specific element, or the host page.

By the ScreenshotNeo team4 October 202611 min read

To screenshot a logged-in web app inside an iframe, authenticate in the same Playwright browser context that will load the host page, wait for a visible signal that the embedded app is signed in and ready, then capture the iframe or a specific element inside it. Capture the host page instead when you need the surrounding layout. The login flow, frame selector, and readiness signal depend on the app.

This guide uses Playwright with JavaScript. The examples are templates: replace the example URLs and selectors with values from your app, and use an account and capture process you are authorized to use. Playwright’s authentication guide, FrameLocator API, and screenshot API document the APIs used here.

1. Install Playwright and prepare the browser

Create a project and install Playwright. The browser installation command downloads the browser binaries Playwright uses.

mkdir iframe-screenshots
cd iframe-screenshots
npm init -y
npm install playwright
npx playwright install chromium

The examples use ES modules. Add "type": "module" to the package.json file, or save the scripts with a .mjs extension.

2. Authenticate and save browser state

For a repeatable capture, sign in once, save the resulting browser state, then load it into the context used for screenshots. This example shows a basic username-and-password form. Change the selectors and login steps to match your app; handle multi-factor authentication or identity-provider redirects according to your organization’s workflow.

// save-auth.js
import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();

await page.goto('https://app.example.com/login', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Email').fill(process.env.APP_EMAIL);
await page.getByLabel('Password').fill(process.env.APP_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();

// Replace this with a reliable, app-specific signed-in signal.
await page.getByRole('heading', { name: 'Dashboard' }).waitFor({ state: 'visible' });

await context.storageState({ path: 'playwright/.auth/user.json' });
await browser.close();

Run it with credentials supplied through the environment rather than written into the script:

mkdir -p playwright/.auth
APP_EMAIL='you@example.com' APP_PASSWORD='your-secret' node save-auth.js

Keep playwright/.auth/user.json out of source control. It can contain cookies and other state that lets someone act as the account. Restrict access to it, avoid printing its contents, and refresh it when the session expires. For automation in CI, use your secret store and a secure artifact policy.

3. Load the host page and capture the iframe

Restore the saved state into a browser context, navigate to the host page, identify the iframe, and wait for an app-specific marker inside it. Then capture the iframe’s rendered bounds.

// capture-iframe.js
import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  storageState: 'playwright/.auth/user.json',
  viewport: { width: 1440, height: 1000 },
  deviceScaleFactor: 1,
});
const page = await context.newPage();

await page.goto('https://host.example.com/page-with-iframe', {
  waitUntil: 'domcontentloaded',
});

const iframeSelector = 'iframe[title="App"]';
const app = page.frameLocator(iframeSelector);

// Use a visible marker that appears only after the app is authenticated and ready.
await app.getByRole('heading', { name: 'Dashboard' }).waitFor({
  state: 'visible',
  timeout: 30_000,
});

await page.locator(iframeSelector).screenshot({
  path: 'iframe.png',
  animations: 'disabled',
  caret: 'hide',
});

await browser.close();

Run the capture:

node capture-iframe.js

A frame locator lets you find content inside an iframe. The iframe element locator is used above for the screenshot so the result includes the embedded app’s visible rectangle. To capture only an inner component, screenshot a locator inside the frame instead:

await app.getByTestId('revenue-chart').screenshot({ path: 'chart.png' });

Use a stable selector such as an iframe title, test ID, or distinctive accessible name. If the page has multiple matching frames, narrow the selector; frame locators are strict and ambiguous matches can cause an error.

4. Choose what pixels to capture

Goal Playwright capture What it includes
Only the embedded app page.locator(iframeSelector).screenshot(...) The iframe element’s rendered bounds.
A component inside the app app.getByTestId('...').screenshot(...) The selected element within the iframe.
Host page viewport page.screenshot(...) The visible browser page, including the iframe and surrounding host content.
Entire scrollable host page page.screenshot({ fullPage: true, ... }) The full scrollable host page. Embedded content must still be rendered and available.

For a host-page viewport image, replace the iframe screenshot call with:

await page.screenshot({ path: 'host-viewport.png', animations: 'disabled' });

For the host page’s full scrollable height:

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

A full-page screenshot changes the host page’s capture extent; it does not guarantee that a nested app’s own internal scroll container is expanded. If the iframe contains a separately scrolling dashboard, scroll that app or target the required content deliberately. Cross-origin restrictions can prevent page scripts from inspecting an iframe’s DOM, but browser automation’s frame APIs can target frames; the app still needs to load and authenticate in the browser.

5. Make authentication work inside the embedded app

Restoring browser state does not guarantee that an embedded third-party app will accept it. The browser sends cookies according to their domain, security, and same-site rules, and the app may use its own origin and login session. Test the exact host page and browser configuration used for capture.

  • Same browser context: perform login and capture in one context, or restore its saved state into the capture context.
  • Origin-specific state: state for the host origin may not sign in a different-origin iframe. Authenticate to the app’s origin when appropriate and permitted.
  • Third-party cookie restrictions: embedded login can behave differently from opening the app directly. Check the browser’s cookie policy and the app’s supported embedding setup.
  • Session storage: Playwright storage state covers cookies and local storage, and can include IndexedDB snapshots. Session storage needs separate handling if the app relies on it.
  • Expired or revoked sessions: verify the actual signed-in marker on every run; do not treat successful navigation as proof of authentication.
  • Identity-provider and MFA flows: complete the supported sign-in flow in the browser. Avoid bypassing access controls or storing reusable credentials in source code.

Playwright’s storageState documentation describes the saved state and its IndexedDB option. For an app whose auth token is stored in IndexedDB, save that snapshot with indexedDB: true if supported by the installed Playwright version:

await context.storageState({ path: 'playwright/.auth/user.json', indexedDB: true });

6. Wait for the app, not just navigation

page.goto() reaching a navigation milestone does not mean an asynchronously loaded iframe has finished rendering or fetched its data. Wait for an app-specific, visible readiness marker: a dashboard heading, account name, loaded-data label, or known chart. Prefer this over a fixed sleep.

// Better: wait for an app-specific signal
await app.getByText('Account: Acme').waitFor({ state: 'visible', timeout: 30_000 });

// Use a delay only when the app has a known delayed transition and no observable signal.
await page.waitForTimeout(1500);

If the app has a loading indicator, wait for it to disappear and then wait for the expected content. A single marker can appear before all charts or images finish rendering, so choose a signal that represents the output you need.

7. Adjust screenshot output

Playwright’s screenshot methods accept options for format, quality, scale, clipping, animation handling, masks, and more. Select only options relevant to the output:

Option Use
path Save the result; the extension can determine PNG, JPEG, or WebP.
type Set png, jpeg, or webp explicitly.
quality Set lossy image quality from 0 to 100 for JPEG or WebP; not applicable to PNG.
scale css produces one output pixel per CSS pixel; device uses device pixels and can produce larger images.
fullPage Capture the full scrollable page instead of the viewport (page screenshot).
clip Capture a rectangle using x, y, width, and height coordinates.
animations Use disabled for more repeatable screenshots, or allow to preserve motion state.
mask and maskColor Cover sensitive or dynamic elements identified by locators with a solid color.
style Apply a stylesheet for capture, such as hiding an unstable banner.
omitBackground Allow transparency where supported; it does not apply to JPEG.
timeout Set a screenshot operation timeout; browser context or page defaults can also control timeouts.

Example: save a compressed WebP of the iframe and mask a user-specific email. The locator used for masking should be checked against the app’s actual DOM.

await page.locator(iframeSelector).screenshot({
  path: 'iframe.webp',
  type: 'webp',
  quality: 85,
  scale: 'css',
  animations: 'disabled',
  mask: [app.getByTestId('user-email')],
});

For the full option list and version-specific details, see the Playwright screenshot API.

8. cURL, Python, and Node.js options

Browser automation is the appropriate route when the screenshot requires a real browser session, interactive login, or authenticated iframe state. A plain HTTP request with cURL or Python does not run the web app, execute its JavaScript, or automatically reproduce the browser’s login and iframe behavior. These tools are useful for calling an existing screenshot service or endpoint when the page is publicly accessible and does not require a private browser session.

cURL: call a screenshot endpoint

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python: call a screenshot endpoint

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: call a screenshot endpoint

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(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

These service examples demonstrate URL-to-image capture for an accessible page. They do not transfer your local Playwright storage state or sign in to a private iframe. For private apps, use the Playwright flow above unless the service explicitly supports the required authentication and access model.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. For a page the service can access, one GET request returns an image or PDF; see the ScreenshotNeo API documentation. The service removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, and cache hits are never billed. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

ScreenshotNeo does not receive or reuse the Playwright state file in this guide. Confirm the target page is accessible through the service and do not send private credentials unless the service’s documented authentication options fit your security requirements. Sign up for 1,000 free screenshots a month with no card.

Troubleshooting

Symptom Likely cause Fix
Dashboard marker times out The iframe did not load, the app is still fetching data, the selector is wrong, or the session is signed out. Inspect the rendered page and frame, verify the exact selector, and check the signed-in state in the same context. Increase the timeout only when the app legitimately needs longer.
Frame locator cannot find the target The iframe selector is wrong, the frame is attached later, or more than one iframe matches. Wait for the iframe locator to be visible, use a stable title or test ID, and disambiguate multiple matches.
Screenshot is a login page Saved state expired, belongs to another origin, or is not accepted in the embedded context. Re-authenticate, verify the account marker inside the frame, and check the app’s embedding and cookie requirements.
Screenshot shows a spinner or incomplete chart Navigation completed before app data or visual rendering completed. Wait for the data-ready marker or chart element. Avoid relying solely on load or a fixed delay.
Only part of the dashboard appears The iframe has a fixed viewport or the app scrolls inside its own container. Capture the desired inner element, scroll the iframe app deliberately, or adjust the host layout where you control it.
Screenshot has the wrong dimensions or is unexpectedly large Viewport, device scale factor, or screenshot scale differs from expectations. Set context viewport and deviceScaleFactor explicitly; choose scale: 'css' for CSS-pixel output.
Auth works locally but fails in CI State file is missing, expired, inaccessible, or incompatible with the CI browser configuration. Provision it securely in CI, reproduce the same browser/context settings, and refresh state through the supported login flow.
Storage state does not preserve sign-in The app uses session storage or another auth store, or the relevant origin was not included. Check the app’s storage mechanism; include IndexedDB in storage state when applicable and handle session storage explicitly if needed.

Performance, reliability, and cost

  • Reuse state carefully: logging in for every screenshot adds navigation and authentication work. Reusing state can reduce setup, but sessions expire and must be checked.
  • Use a deliberate viewport and scale: large full-page or device-scale captures use more memory and produce larger files. Capture only the required area and use CSS scale when device pixels are unnecessary.
  • Wait on meaningful conditions: app-specific readiness checks improve reliability and avoid arbitrary sleep time. Use bounded timeouts and surface failures rather than saving misleading partial images.
  • Close resources: close pages, contexts, and browsers after capture, especially in batch jobs. Limit concurrency to what the host can support.
  • Protect data: screenshots can contain account details, and storage-state files can carry credentials. Restrict access, retention, and logging.
  • Cost: Playwright is browser automation you operate, so resource costs depend on your execution environment and workload; no universal price or timing applies. A managed screenshot API may reduce browser maintenance for publicly accessible pages, but verify authentication support, billing rules, and data handling for your use case.

FAQ

Can I screenshot a cross-origin iframe?

Browser automation can target a frame through Playwright’s frame APIs. Cross-origin rules still affect what page JavaScript can inspect, and the embedded app must load and authenticate successfully in the browser context.

Can I capture a logged-in iframe with a URL-only screenshot API?

Only if the service supports the target’s required authentication and access model. A URL-only request does not automatically inherit a local Playwright session.

Should I capture the iframe or the whole page?

Capture the iframe element for the embedded app’s visible rectangle, an inner locator for a component, or the host page for surrounding context. Use full-page capture when you need below-the-fold host content.

What should I use as the readiness check?

Choose a visible marker that appears only when the signed-in app has reached the state you need, such as a dashboard heading or loaded-data label. There is no universal selector or wait duration.