How to Handle Iframe Targeting and Nested Browsing Contexts in ego-browser

To handle iframe targeting in ego-browser, use browser.iframeTarget(substring) to discover a CDP target by URL, then attach to it with Target.attachToTarget to create a session that routes all subsequent commands into the nested browsing context.

Working with nested frames in browser automation is notoriously complex. The ego-browser package in the citrolabs/ego-lite repository simplifies this by exposing every iframe as a separate Chrome DevTools Protocol (CDP) target with its own targetId. This architecture lets you discover, attach to, and control iframes using the same high-level API you use for top-level pages.

Discovering Iframe Targets with iframeTarget

The entry point for iframe handling is the iframeTarget method. According to the source code in driver/nav.ts, this helper scans all available CDP targets and returns the targetId of the first iframe whose URL contains your supplied substring.

The function is re-exported from helpers.ts for convenient access:

import { browser } from 'ego-browser';

// Find an iframe with "checkout" in its URL
const iframeId = await browser.iframeTarget('checkout');
console.log('Found iframe target:', iframeId);

This is more reliable than DOM-based iframe detection because it operates at the browser target level, catching dynamically created iframes that may not yet be in the DOM tree.

Attaching to a Nested Browsing Context

Once you have the targetId, you must create a CDP session bound to that specific target. The cdp helper—also exported from helpers.ts—sends the Target.attachToTarget command:

import { cdp } from 'ego-browser';

const { sessionId } = await cdp('Target.attachToTarget', {
  targetId: iframeId,
  flatten: true,  // Required for modern session handling
});

console.log('Attached session:', sessionId);

The sessionId returned in the response is the key to iframe targeting. All subsequent CDP commands that include this sessionId are automatically routed to the iframe's browsing context rather than the main page.

Executing Commands Inside Iframes

With an active session, you can run any CDP command in the nested context. The ego-browser runtime handles the routing transparently:

// Evaluate JavaScript inside the iframe
const result = await cdp('Runtime.evaluate', {
  expression: 'document.title',
  returnByValue: true,
  sessionId,  // Routes to iframe context
});

console.log('Iframe title:', result.result.value);

This works for any CDP domain: DOM.querySelector, Input.dispatchMouseEvent, Network.*, and more. The sessionId parameter is what distinguishes nested browsing context commands from main page commands.

Automatic Session Management in Higher-Level APIs

The runtime maintains an internal mapping of iframe targets to sessions. In element-resolver.ts, the iframeSessions Map stores these relationships. This enables transparent operation through the high-level API without manual session tracking:

// Attach once, then use standard API
const iframeId = await browser.iframeTarget('checkout');
await cdp('Target.attachToTarget', { 
  targetId: iframeId, 
  flatten: true 
});

// These now automatically run inside the iframe
const title = await page.evaluate(() => document.title);
const button = await page.locator('#submit');
await button.click();

The page.locator and page.evaluate methods query the iframeSessions map in element-resolver.ts to determine the correct execution context for each operation.

Cleaning Up Iframe Sessions

When finished with an iframe target, explicitly detach to free resources:

await cdp('Target.detachFromTarget', { sessionId });

If omitted, the runtime in browser-runtime.ts handles cleanup on process termination, but explicit detachment is recommended for long-running automation scripts with many frame transitions.

Complete Working Example

Here's a full pattern for reliable iframe targeting:

import { browser, cdp, page } from 'ego-browser';

async function interactWithIframe() {
  // Step 1: Discover
  const iframeId = await browser.iframeTarget('payment-form');
  
  // Step 2: Attach
  const { sessionId } = await cdp('Target.attachToTarget', {
    targetId: iframeId,
    flatten: true,
  });
  
  // Step 3: Interact (low-level CDP)
  await cdp('Input.insertText', {
    text: '4111111111111111',
    sessionId,
  });
  
  // Or use high-level API (automatic context detection)
  const submitButton = await page.locator('.pay-btn');
  await submitButton.click();
  
  // Step 4: Cleanup
  await cdp('Target.detachFromTarget', { sessionId });
}

await interactWithIframe();

Key Source Files for Iframe Handling

File Purpose
helpers.ts Re-exports iframeTarget and generic cdp helper
driver/nav.ts Implements iframeTarget by filtering Target.getTargets
browser-runtime.ts Manages CDP session lifecycle including attach/detach
element-resolver.ts Maintains iframeSessions Map for automatic context routing

Summary

  • browser.iframeTarget(substring) in driver/nav.ts discovers iframe CDP targets by URL pattern
  • cdp('Target.attachToTarget', { targetId, flatten: true }) creates a session for a nested browsing context
  • The sessionId routes all subsequent commands to the iframe instead of the main page
  • element-resolver.ts enables transparent high-level API usage after attachment
  • Explicit Target.detachFromTarget releases resources when done

Frequently Asked Questions

How does ego-browser identify iframes compared to Puppeteer or Playwright?

Unlike Puppeteer/Playwright, which abstract iframes as page-like objects, ego-browser exposes the underlying Chrome DevTools Protocol target structure directly. Every frame—including main pages, subframes, and worker contexts—is a separate CDP target with a unique targetId. The iframeTarget helper filters these targets by URL substring, giving you precise control without framework abstractions.

Can I attach to multiple nested iframes simultaneously?

Yes. Each Target.attachToTarget call returns a distinct sessionId. The iframeSessions Map in element-resolver.ts tracks these independently. You can maintain active sessions for multiple iframes and switch between them by using the appropriate sessionId in CDP calls, or rely on automatic resolution through page.locator when selectors uniquely identify elements in specific frames.

What happens if an iframe navigates to a new URL after I attach?

The sessionId remains valid across navigations within the same targetId. However, if the iframe is destroyed and recreated (common with single-page applications), the original targetId becomes invalid. You must call iframeTarget again to discover the new target and create a fresh session. The runtime does not automatically remap destroyed frames.

Is the flatten: true parameter required when attaching to iframes?

Yes. Modern CDP uses flattened sessions where commands directly specify sessionId rather than nesting through Target.sendMessageToTarget. The ego-browser implementation in browser-runtime.ts assumes flattened mode. Omitting flatten: true will cause session routing failures for all subsequent commands.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →