Q27 of 42 · Playwright

How do you handle iframes in Playwright?

PlaywrightMidplaywrightiframesframe-locatormid

Short answer

Short answer: Use `page.frameLocator(selector)` to get a frame-aware Locator, then chain normal Locator APIs. `frameLocator` works for same-origin and cross-origin iframes uniformly. Older API: `page.frame({ name })` returns a Frame object you can drive directly.

Detail

Playwright treats iframes as first-class and works the same way for cross-origin frames as for same-origin. The modern API is frameLocator:

await page.goto('/checkout');

const stripe = page.frameLocator('iframe[name=stripe-card]');
await stripe.getByLabel('Card number').fill('4242424242424242');
await stripe.getByLabel('Expiry').fill('12/30');
await stripe.getByLabel('CVC').fill('123');

The chain reads like a regular Locator chain — auto-wait, semantic locators, the lot.

frameLocator chain:

  • Lazily resolves the frame each time you act, so it survives navigation/replacement.
  • Combines naturally with .filter, .nth, .locator(...).
  • Works for cross-origin without any special config (architectural advantage over Cypress).

Older API: page.frame(...) and page.frames() — returns a Frame object that you can drive with the same methods as page (.click, .fill, .locator). Useful when you need the Frame's own properties (url, name, parentFrame).

const frame = page.frame({ name: 'stripe-card' });
await frame?.locator('[name=cardnumber]').fill('4242 4242 4242 4242');

Nested iframes:

const inner = page.frameLocator('iframe.outer').frameLocator('iframe.inner');
await inner.getByLabel('Field').fill('value');

Common pattern: verify iframe content loaded:

await expect(stripe.getByLabel('Card number')).toBeVisible();

The same auto-wait that makes regular Locators robust applies inside frames.

For payment-style iframes specifically, prefer the provider's test cards (Stripe's 4242..., Adyen's test BINs). They're more reliable than fighting iframe boundaries with custom logic.

// EXAMPLE

iframe.spec.ts

import { test, expect } from '@playwright/test';

test('fills a Stripe card iframe', async ({ page }) => {
  await page.goto('/checkout');

  const card = page.frameLocator('iframe[title*="card"]');
  await card.getByPlaceholder('1234 1234 1234 1234').fill('4242424242424242');
  await card.getByPlaceholder('MM / YY').fill('12 / 30');
  await card.getByPlaceholder('CVC').fill('123');

  await page.getByRole('button', { name: 'Pay' }).click();
  await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
});

// WHAT INTERVIEWERS LOOK FOR

Knowing `frameLocator` is the modern API, that it works cross-origin, and the chain pattern with semantic locators inside.

// COMMON PITFALL

Reaching for the old `page.frame()` API by reflex from Puppeteer — `frameLocator` is the modern, retry-aware alternative.