You can interact with content inside iframes in Playwright just like you would with any other element on the page, but you need to tell Playwright which iframe you’re targeting first.
Here’s a simple HTML page with an iframe:
<!DOCTYPE html>
<html>
<head>
<title>iFrame Example</title>
</head>
<body>
<h1>Main Page</h1>
<iframe id="my-iframe" srcdoc="
<!DOCTYPE html>
<html>
<head>
<title>iFrame Content</title>
</head>
<body>
<h2>Inside the iFrame</h2>
<p id='iframe-text'>This text is in the iframe.</p>
<button onclick='alert(\"Hello from iframe!\")'>Click Me</button>
</body>
</html>
"></iframe>
<p>Content outside the iframe.</p>
</body>
</html>
And here’s how you’d interact with it using Playwright:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content("""
<!DOCTYPE html>
<html>
<head>
<title>iFrame Example</title>
</head>
<body>
<h1>Main Page</h1>
<iframe id="my-iframe" srcdoc="
<!DOCTYPE html>
<html>
<head>
<title>iFrame Content</title>
</head>
<body>
<h2>Inside the iFrame</h2>
<p id='iframe-text'>This text is in the iframe.</p>
<button onclick='alert(\"Hello from iframe!\")'>Click Me</button>
</body>
</html>
"></iframe>
<p>Content outside the iframe.</p>
</body>
</html>
""")
# Get a handle to the iframe element
iframe_element = page.locator("#my-iframe")
# Switch to the content of the iframe
iframe_page = iframe_element.content_frame()
# Interact with elements inside the iframe
iframe_text = iframe_page.locator("#iframe-text")
print(f"Text inside iframe: {iframe_text.text_content()}")
iframe_button = iframe_page.locator("button")
iframe_button.click() # This will trigger the alert
# You can also interact directly without getting the frame object explicitly
page.locator("#my-iframe").locator("#iframe-text").text_content()
browser.close()
The core idea is that Playwright treats each iframe as a separate "frame" that you can navigate into. You first locate the <iframe> HTML element itself, and then use the .content_frame() method on that locator to get a Frame object. Once you have the Frame object, you can use all the standard Playwright locators and actions on it, as if it were the main page.
This allows you to test complex UIs where third-party widgets, ads, or distinct sections of your application are loaded within iframes. Playwright’s ability to target these frames makes it seamless to interact with them.
When you have multiple nested iframes, you can chain these .content_frame() calls. For example, if you had an iframe within an iframe, you’d do parent_frame.locator("selector_for_child_iframe").content_frame().
Consider a scenario where the iframe itself is dynamically loaded. You might need to wait for the iframe to appear or for its content to be ready before attempting to interact with it. Playwright’s auto-waiting for locators handles most of this automatically, but if you encounter issues, explicitly waiting for the iframe element or its content can be helpful.
The .content_frame() method returns None if the src attribute of the iframe element is empty or points to a non-HTML resource (like an image), or if the iframe has not yet loaded its content.
The most surprising thing about interacting with iframes is that Playwright’s API makes it feel no different than interacting with elements on the main page, abstracting away the complexities of cross-origin policies and DOM isolation that would typically make this harder. You simply target the iframe element and then operate within its context.
The frame_locator API is a more modern and often cleaner way to handle iframes, especially nested ones. Instead of getting the iframe element and then calling .content_frame(), you can directly chain .frame_locator() calls.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content("""
<!DOCTYPE html>
<html>
<head>
<title>iFrame Example</title>
</head>
<body>
<h1>Main Page</h1>
<iframe id="my-iframe" srcdoc="
<!DOCTYPE html>
<html>
<head>
<title>iFrame Content</title>
</head>
<body>
<h2>Inside the iFrame</h2>
<p id='iframe-text'>This text is in the iframe.</p>
<button onclick='alert(\"Hello from iframe!\")'>Click Me</button>
</body>
</html>
"></iframe>
<p>Content outside the iframe.</p>
</body>
</html>
""")
# Use frame_locator to directly target elements within the iframe
iframe_text = page.frame_locator("#my-iframe").locator("#iframe-text")
print(f"Text inside iframe: {iframe_text.text_content()}")
page.frame_locator("#my-iframe").locator("button").click()
browser.close()
This frame_locator approach is generally preferred as it makes the intention clearer and can simplify complex nesting scenarios. You can even chain multiple frame_locator calls if you have iframes within iframes: page.frame_locator("outer-iframe-selector").frame_locator("inner-iframe-selector").locator("element-in-inner-iframe").
When dealing with iframes that are served from a different origin than the main page, Playwright’s cross-origin capabilities allow you to interact with them seamlessly, provided the browser context allows it. This is crucial for testing scenarios involving third-party integrations or complex micro-frontend architectures.
The next logical step after mastering basic iframe interaction is handling scenarios with dynamic iframe loading or iframes that require specific navigation within them.