← Back

Playwright Frames, Windows & Tabs Handling

Learn how Playwright handles iframes, multiple pages, browser tabs, popup windows and cross-page workflows with practical real-world examples — plus Selenium comparison.

1. What Are Frames, Windows and Tabs?

Modern web applications frequently use embedded content, new tabs and multiple browser pages. Automation engineers need to identify which browser object they are interacting with.

Frame / iframe A document embedded inside another HTML document.
Page A Playwright representation of a browser tab or page.
Popup A new page created as a result of an action such as clicking a link or button.
Browser Context An isolated browser session containing one or more pages.
2. Important Mental Model
Browser
→
BrowserContext
→
Page / Tab
→
Frame
Remember:

A normal browser tab is represented by a Playwright Page.

An iframe is represented by a Playwright Frame.

Multiple tabs normally mean multiple Page objects inside the same browser context.
3. Handling an iframe in Playwright

Suppose a payment application embeds a payment form inside an iframe.

Application HTML
<iframe
    src="/payment-form"
    title="Payment Form">
</iframe>

The fields inside the iframe belong to the iframe document, not directly to the main page.

Using frame locator

Playwright Python
payment_frame = page.frame_locator(
    'iframe[title="Payment Form"]'
)

payment_frame.get_by_label(
    "Card Number"
).fill("4111111111111111")

payment_frame.get_by_label(
    "Expiry"
).fill("12/30")

payment_frame.get_by_label(
    "CVV"
).fill("123")
Professional approach:
frame_locator() allows you to locate elements inside an iframe without manually switching the browser context like traditional window switching.
4. Real-Time Example — Payment Gateway iframe

A common real-world application is an e-commerce checkout page where the payment provider renders card fields inside an iframe.

Shopping Cart
→
Checkout
→
Payment iframe
→
Enter Card
→
Pay
Real-Time Playwright Test
from playwright.sync_api import expect


def test_checkout_payment(page):

    page.goto(
        "https://example.test/checkout"
    )

    payment_frame = page.frame_locator(
        'iframe[title="Payment Form"]'
    )

    payment_frame.get_by_label(
        "Card Number"
    ).fill("4111111111111111")

    payment_frame.get_by_label(
        "Expiry"
    ).fill("12/30")

    payment_frame.get_by_label(
        "CVV"
    ).fill("123")

    payment_frame.get_by_role(
        "button",
        name="Pay"
    ).click()

    expect(
        page.get_by_text(
            "Payment successful"
        )
    ).to_be_visible()
5. iframe Using a Frame Locator

The frame locator can be chained to interact with elements inside the frame.

Nested iframe example
frame = page.frame_locator(
    "#customer-frame"
)

frame.get_by_label(
    "First Name"
).fill("Rahul")

frame.get_by_label(
    "Last Name"
).fill("Sharma")

frame.get_by_role(
    "button",
    name="Save"
).click()
Useful idea:
Main page locator: page.get_by_label(...)

iframe locator: page.frame_locator(...).get_by_label(...)
6. Locate an iframe by URL

Sometimes an iframe does not have a useful title or stable CSS selector. You can locate it using a URL pattern.

Playwright Python
payment_frame = page.frame_locator(
    'iframe[src*="payment"]'
)

payment_frame.get_by_label(
    "Card Number"
).fill("4111111111111111")
Locator advice: Prefer stable attributes such as title, data-testid or a stable application-specific attribute when available. Avoid relying on a long generated CSS/XPath expression.
7. Access Frames Through page.frames

Playwright also exposes the frames currently attached to a page.

Playwright Python
for frame in page.frames:

    print(
        "Frame URL:",
        frame.url
    )

    print(
        "Frame name:",
        frame.name
    )

This is particularly useful while debugging applications containing several iframes.

8. Find a Specific Frame

If the application has a named frame, you can inspect the available frames and select the appropriate one.

Playwright Python
payment_frame = page.frame(
    name="paymentFrame"
)

assert payment_frame is not None

payment_frame.get_by_label(
    "Card Number"
).fill("4111111111111111")
Preferred for most test code:
Use frame_locator() when you can identify the iframe with a stable locator. It keeps the interaction style close to normal Playwright locator usage.
9. What Is a Browser Tab in Playwright?

In Playwright, a browser tab is represented by a Page.

BrowserContext
→
Page 1
Main Tab
+
Page 2
New Tab
+
Page 3
Another Tab

You normally do not need to perform a traditional "switch-to-window" operation.

10. Open a New Page Manually

A new page can be created directly from the browser context.

Playwright Python
context = page.context

new_page = context.new_page()

new_page.goto(
    "https://example.test/reports"
)

print(new_page.url)
Important:
context.new_page() creates another page in the same browser context. It shares the context's session state, subject to how that context was configured.
11. Real-Time Example — Admin and Reports Tabs

Imagine an admin application where the administrator keeps the dashboard open and opens a report in another tab.

Admin Dashboard
→
Click Reports
→
Report Tab
→
Validate Report
Playwright Python
from playwright.sync_api import expect


def test_admin_report(page):

    page.goto(
        "https://example.test/admin"
    )

    with page.expect_popup() as popup_info:

        page.get_by_role(
            "link",
            name="Open Sales Report"
        ).click()

    report_page = popup_info.value

    expect(
        report_page.get_by_role(
            "heading",
            name="Sales Report"
        )
    ).to_be_visible()

    expect(
        report_page
    ).to_have_title(
        "Sales Report"
    )

    print(
        "Report URL:",
        report_page.url
    )
12. Why Use expect_popup()?

If a click is expected to open another page, use expect_popup() around the action.

Preferred pattern
with page.expect_popup() as popup_info:

    page.get_by_role(
        "link",
        name="Open Invoice"
    ).click()

invoice_page = popup_info.value

invoice_page.wait_for_load_state()

print(invoice_page.url)
Why this pattern is useful:
The code explicitly connects the user action with the new page event expected from that action.
13. Popup Validation

Opening the popup is not enough. A professional test should validate the business result.

Popup validation
with page.expect_popup() as popup_info:

    page.get_by_role(
        "link",
        name="Download Invoice"
    ).click()

invoice_page = popup_info.value

expect(
    invoice_page
).to_have_title(
    "Invoice"
)

expect(
    invoice_page.get_by_text(
        "Invoice Number"
    )
).to_be_visible()

assert "invoice" in (
    invoice_page.url.lower()
)
14. Multiple Tabs at the Same Time

You can keep references to multiple pages and interact with them independently.

Multiple Playwright Pages
context = page.context

customer_page = context.new_page()
admin_page = context.new_page()

customer_page.goto(
    "https://example.test/customer"
)

admin_page.goto(
    "https://example.test/admin"
)

print(
    "Customer:",
    customer_page.url
)

print(
    "Admin:",
    admin_page.url
)
You can work with both page objects without repeatedly switching a global driver object.
15. Multiple Pages Created by an Application

You can inspect pages currently available in the browser context.

Playwright Python
for browser_page in page.context.pages:

    print(
        "Page URL:",
        browser_page.url
    )

This can be useful for debugging applications that unexpectedly open additional tabs.

16. Real-Time Example — Customer Portal + Support Portal

Suppose an agent opens a customer portal and a support portal in separate tabs.

Professional Multi-Page Example
from playwright.sync_api import expect


def test_customer_and_support_portals(page):

    context = page.context

    customer_page = context.new_page()
    support_page = context.new_page()

    customer_page.goto(
        "https://example.test/customer"
    )

    support_page.goto(
        "https://example.test/support"
    )

    expect(
        customer_page.get_by_role(
            "heading",
            name="Customer Portal"
        )
    ).to_be_visible()

    expect(
        support_page.get_by_role(
            "heading",
            name="Support Portal"
        )
    ).to_be_visible()
17. Frames + Popup Together

Real applications can combine frames and multiple pages. For example, a checkout page can open a payment provider in a new tab, where the payment form itself is inside an iframe.

Checkout Page
→
Payment Popup
→
Payment iframe
→
Submit Payment
Combined Playwright Example
from playwright.sync_api import expect


def test_external_payment(page):

    page.goto(
        "https://example.test/checkout"
    )

    with page.expect_popup() as popup_info:

        page.get_by_role(
            "button",
            name="Open Payment"
        ).click()

    payment_page = popup_info.value

    payment_frame = payment_page.frame_locator(
        'iframe[title="Payment Form"]'
    )

    payment_frame.get_by_label(
        "Card Number"
    ).fill("4111111111111111")

    payment_frame.get_by_label(
        "Expiry"
    ).fill("12/30")

    payment_frame.get_by_label(
        "CVV"
    ).fill("123")

    payment_frame.get_by_role(
        "button",
        name="Pay"
    ).click()

    expect(
        payment_page.get_by_text(
            "Payment successful"
        )
    ).to_be_visible()
Mental model:

Main Page → Popup Page → Frame → Element
18. Selenium iframe Handling

Selenium traditionally requires switching the driver context into the iframe before interacting with elements inside it.

Selenium Python
from selenium.webdriver.common.by import By

iframe = driver.find_element(
    By.CSS_SELECTOR,
    'iframe[title="Payment Form"]'
)

driver.switch_to.frame(iframe)

driver.find_element(
    By.ID,
    "cardNumber"
).send_keys(
    "4111111111111111"
)

driver.find_element(
    By.ID,
    "expiry"
).send_keys(
    "12/30"
)

driver.find_element(
    By.ID,
    "cvv"
).send_keys(
    "123"
)

driver.switch_to.default_content()
Selenium's switch_to.frame() changes the current driver context. After completing the iframe operation, you often need to switch back using default_content().
19. Selenium Nested Frame Handling

For nested frames, Selenium can switch into each frame sequentially.

Selenium Python
outer_frame = driver.find_element(
    By.ID,
    "outerFrame"
)

driver.switch_to.frame(
    outer_frame
)

inner_frame = driver.find_element(
    By.ID,
    "innerFrame"
)

driver.switch_to.frame(
    inner_frame
)

driver.find_element(
    By.ID,
    "customerName"
).send_keys(
    "Rahul"
)

driver.switch_to.default_content()
20. Selenium Multiple Windows / Tabs

Selenium uses window handles to identify different browser windows or tabs.

Selenium Python
main_window = (
    driver.current_window_handle
)

driver.find_element(
    By.ID,
    "openReport"
).click()

windows = driver.window_handles

for window in windows:

    if window != main_window:

        driver.switch_to.window(
            window
        )

        break

print(
    "Popup URL:",
    driver.current_url
)
21. Selenium Return to Main Window
Selenium Python
main_window = (
    driver.current_window_handle
)

# Switch to popup
driver.switch_to.window(
    popup_window
)

# Work with popup
print(driver.title)

# Return to original tab
driver.switch_to.window(
    main_window
)
22. Playwright vs Selenium — Frames
Task Playwright Selenium
Locate iframe page.frame_locator() Locate iframe element first.
Interact inside iframe Use frame locator directly. driver.switch_to.frame()
Return to main document Continue using page locator. driver.switch_to.default_content()
Nested frames Chain frame locators. Switch through frames sequentially.
23. Playwright vs Selenium — Windows & Tabs
Task Playwright Selenium
Browser tab Page Window/tab represented by a window handle.
Open new page context.new_page() Usually create/open a new browser tab/window and work with handles.
Popup generated by click page.expect_popup() Inspect window_handles.
Switch to another tab Use the corresponding Page object. driver.switch_to.window()
Return to original tab Continue using the original Page. Switch back using the original window handle.
24. Same Real-Time Scenario — Playwright vs Selenium

Scenario: An administrator clicks Open Customer Report. A new tab opens and the report must be validated.

Playwright

Playwright Python
with page.expect_popup() as popup_info:

    page.get_by_role(
        "link",
        name="Open Customer Report"
    ).click()

report_page = popup_info.value

expect(
    report_page.get_by_role(
        "heading",
        name="Customer Report"
    )
).to_be_visible()

Selenium

Selenium Python
main_window = (
    driver.current_window_handle
)

driver.find_element(
    By.ID,
    "customerReport"
).click()

for window in driver.window_handles:

    if window != main_window:

        driver.switch_to.window(
            window
        )

        break

assert (
    "Customer Report"
    in driver.title
)
25. Playwright vs Selenium — Complete Comparison
Area Playwright Selenium
iframe frame_locator() switch_to.frame()
Main document after iframe Continue using page locators. switch_to.default_content()
New tab New Page Window handle
Popup created by click expect_popup() Inspect window handles
Switch tab Use the page reference. switch_to.window()
Multiple pages Multiple Page objects. Multiple window handles.
Browser context isolation Built-in BrowserContext. Usually managed through separate driver sessions or other test infrastructure.
Event-based popup handling Dedicated popup event API. Commonly handled through window handles.
26. Avoid Hard Waits for New Tabs

❌ Avoid

page.get_by_role( "link", name="Open Report" ).click() page.wait_for_timeout(3000) # Search through pages and hope the popup is ready

✅ Preferred

with page.expect_popup() as popup_info: page.get_by_role( "link", name="Open Report" ).click() report_page = popup_info.value expect( report_page.get_by_role( "heading", name="Report" ) ).to_be_visible()
Wait for the browser event you actually expect instead of sleeping for an arbitrary amount of time.
27. Avoid Fragile iframe Handling

❌ Avoid

driver.find_element( "xpath", "/html/body/div[3]/iframe" )

✅ Preferred

payment_frame = page.frame_locator( 'iframe[title="Payment Form"]' ) payment_frame.get_by_label( "Card Number" ).fill( "4111111111111111" )
28. Professional E-Commerce Example

Here is a realistic workflow combining:

Professional Playwright Test
from playwright.sync_api import expect


def test_complete_checkout(page):

    # --------------------------------
    # 1. Open checkout
    # --------------------------------

    page.goto(
        "https://example.test/checkout"
    )


    # --------------------------------
    # 2. Open payment provider
    # --------------------------------

    with page.expect_popup() as popup_info:

        page.get_by_role(
            "button",
            name="Pay Securely"
        ).click()

    payment_page = popup_info.value


    # --------------------------------
    # 3. Find payment iframe
    # --------------------------------

    payment_frame = (
        payment_page
        .frame_locator(
            'iframe[title="Payment Form"]'
        )
    )


    # --------------------------------
    # 4. Fill payment details
    # --------------------------------

    payment_frame.get_by_label(
        "Card Number"
    ).fill(
        "4111111111111111"
    )

    payment_frame.get_by_label(
        "Expiry"
    ).fill(
        "12/30"
    )

    payment_frame.get_by_label(
        "CVV"
    ).fill(
        "123"
    )


    # --------------------------------
    # 5. Submit payment
    # --------------------------------

    payment_frame.get_by_role(
        "button",
        name="Pay"
    ).click()


    # --------------------------------
    # 6. Validate result
    # --------------------------------

    expect(
        payment_page.get_by_text(
            "Payment successful"
        )
    ).to_be_visible()
Checkout
→
Payment Tab
→
Payment iframe
→
Fill Form
→
Validate
29. Common Mistakes
Mistake Preferred Approach
Trying to locate iframe elements from the main page. Use frame_locator().
Using time.sleep() before a popup. Use expect_popup().
Treating a new tab as a JavaScript dialog. Handle it as a new Page.
Using Selenium-style window switching in Playwright. Keep references to the relevant Playwright Page objects.
Using unstable iframe XPath. Prefer stable iframe attributes.
Only verifying that a tab opened. Validate title, URL or business content.
Creating shared mutable state between tabs/tests. Keep test data and browser contexts isolated.
30. Quick Reference
Requirement Playwright Python
Find iframe page.frame_locator("iframe")
Access named frame page.frame(name="paymentFrame")
List frames page.frames
Current page page
Create new page page.context.new_page()
Wait for popup page.expect_popup()
Get popup page popup_info.value
List open pages page.context.pages
Popup URL popup_page.url
31. Professional Best Practices
32. Final Mental Model
Main Page
→
New Tab
expect_popup()
→
Page Object
→
iframe
frame_locator()
→
Element
Remember these four patterns:

iframe → page.frame_locator()

New tab created by click → page.expect_popup()

New page created directly → page.context.new_page()

Multiple existing tabs → Keep references to their Page objects.
Simple learning formula:

Frame → frame_locator()
Popup → expect_popup()
New Tab → Page
Multiple Tabs → Multiple Page objects
Selenium → Frame switching + Window Handles