Q27 of 42 · Playwright
How do you handle iframes in Playwright?
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();
});