An iframe is a webpage embedded inside another webpage. Payment forms, video players, third-party widgets, CAPTCHAs -- all iframes. The problem? Playwright locators cannot see inside iframes by default. page.getByRole('button') will never find a button that lives inside an iframe. You need frameLocator().
import { test, expect } from '@playwright/test';
test('interact with elements inside an iframe', async ({ page }) => {
await page.goto('/topics/iframes');
// Locate the iframe by CSS selector
const frame = page.frameLocator('#payment-iframe');
// Now use regular locators inside the frame
await frame.getByLabel('Card Number').fill('4111111111111111');
await frame.getByLabel('Expiry').fill('12/28');
await frame.getByLabel('CVV').fill('123');
await frame.getByRole('button', { name: 'Pay Now' }).click();
// Assertion inside the iframe
await expect(frame.getByText('Payment successful')).toBeVisible();
});import { test } from '@playwright/test';
test('multiple ways to locate iframes', async ({ page }) => {
await page.goto('/topics/iframes');
// By ID
const byId = page.frameLocator('#my-iframe');
// By name attribute
const byName = page.frameLocator('iframe[name="editor-frame"]');
// By src attribute
const bySrc = page.frameLocator('iframe[src*="payment"]');
// By title attribute (best for accessibility)
const byTitle = page.frameLocator('iframe[title="Payment Form"]');
// By nth occurrence when multiple iframes exist
const secondIframe = page.frameLocator('iframe >> nth=1');
// Using data-testid
const byTestId = page.frameLocator('[data-testid="video-player"]');
});Some pages have iframes inside iframes. A common example: a chat widget (first iframe) that contains an embedded form (second iframe). You chain frameLocator() calls.
import { test, expect } from '@playwright/test';
test('interact with nested iframes', async ({ page }) => {
await page.goto('/topics/iframes');
// Outer iframe
const outerFrame = page.frameLocator('#outer-iframe');
// Inner iframe -- chain frameLocator
const innerFrame = outerFrame.frameLocator('#inner-iframe');
// Interact with elements in the deepest iframe
await innerFrame.getByRole('textbox').fill('Hello from nested frame');
await innerFrame.getByRole('button', { name: 'Submit' }).click();
// Verify result in the outer iframe
await expect(outerFrame.getByText('Submitted successfully')).toBeVisible();
});Playwright has two APIs for iframes. frameLocator() is the modern one -- it returns a FrameLocator that you chain with regular locators. page.frame() is the older API -- it returns a Frame object. Use frameLocator() for new code. It is stricter and integrates with auto-waiting.
You cannot use expect() directly on a FrameLocator. You can only assert on elements inside it. So expect(frame).toBeVisible() will NOT work. Use expect(frame.getByText('something')).toBeVisible() instead.
If you are testing a third-party iframe (like Stripe payment), you usually cannot control its content. Focus your tests on verifying that the iframe loads and that your application handles the response correctly. Do not try to automate the internals of third-party iframes in production tests.
Q: How do you handle iframes in Playwright?
A: I use page.frameLocator() to locate the iframe by its CSS selector -- by ID, name, src, or data-testid. Once I have the FrameLocator, I chain regular locators like getByRole() and getByLabel() to interact with elements inside it. For nested iframes, I chain frameLocator() calls. The key difference from Selenium: there is no driver.switchTo().frame(). In Playwright, frameLocator() creates a scoped context. I can interact with the main page and iframe elements in the same test without switching. frameLocator() also auto-waits for the iframe to load.
Key Point: Use page.frameLocator() to access iframe elements. Chain frameLocator() for nested iframes. No switching needed. Auto-waits for the iframe to load.