Controlled Frame

Demián Renzulli
Demián Renzulli
Simon Hangl
Simon Hangl

The <iframe> element is typically used to embed external resources within a browsing context. Iframes enforce the web's security policies by isolating cross-origin embedded content from the host page and vice versa. While this approach enhances security by ensuring a secure boundary between origins, it limits some use cases. For example, users may need to dynamically load and manage content from different sources—such as a teacher triggering a navigation event to display a webpage on a classroom screen. However, many websites explicitly block embedding on iframes using security headers like X-Frame-Options and Content Security Policy (CSP). Additionally, iframe restrictions prevent embedding pages from directly managing the navigation or behavior of the embedded content.

The Controlled Frame API addresses this limitation by allowing the loading of any web content, even if it enforces restrictive embedding policies. This API is exclusively available within Isolated Web Applications (IWAs), which incorporate additional security measures to safeguard both users and developers from potential risks.

Implement Controlled Frames

Before using a Controlled Frame, you'll need to set up a functional IWA. You can then integrate Controlled Frames into your pages.

Add permission policy

To use Controlled Frames, enable the corresponding permission by adding a permissions_policy field with the value "controlled-frame" to your IWA manifest. Additionally, including the cross-origin-isolated key. This key is not specific to Controlled Frames, but is required for all IWAs and determines whether the document can access APIs that require cross-origin isolation.

{
   ...
  "permissions_policy": {
     ...
     "controlled-frame": ["self"],
     "cross-origin-isolated": ["self"]
     ...
  }
   ...
}

The controlled-frame key in an Isolated Web App (IWA) manifest defines a permissions policy allowlist, specifying which origins can use Controlled Frames. While the manifest supports the full Permissions Policy syntax—allowing values like *, specific origins, or keywords like self and src—it is crucial to note that IWA-specific APIs cannot be delegated to other origins. Even if the allowlist includes a wildcard or external origins, these permissions won't take effect for IWA features like controlled-frame. Unlike standard web apps, IWAs default all policy-controlled features to none, requiring explicit declarations. For IWA-specific features, this means that only values like self (the IWA's own origin) or src (the origin of an embedded frame) are functionally effective.

Add a Controlled Frame element

Insert a <controlledframe> element into your HTML to embed third-party content within your IWA.

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

The optional partition attribute configures storage partitioning for embedded content, letting you isolate data such as cookies and local storage to persist data across sessions.

Example: In-memory storage partition

Create a Controlled Frame using an in-memory storage partition named "session1". Data stored in this partition (for example, cookies, and localStorage) will be cleared when the frame is destroyed or the application session ends.

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

Example: Persistent storage partition

Create a Controlled Frame using a persistent storage partition named "user_data". The "persist:" prefix ensures that data stored in this partition is saved to disk and will be available across application sessions.

<controlledframe id="frame_2" src="..." partition="persist:user_data">
</controlledframe>

Get element reference

Obtain a reference to the <controlledframe> element so you can interact with it like any standard HTML element:

const controlledframe = document.getElementById('controlledframe_1');

Frequent scenarios and use cases

As a general rule, choose the best technology to meet your needs while avoiding unnecessary complexity. In recent years, Progressive Web Apps (PWAs) have closed the gap with native apps, enabling powerful web experiences. If a web application needs to embed third-party content, it's recommended to first explore the regular <iframe> approach. If requirements exceed the capabilities of iframes, Controlled Frames on IWAs may be the best alternative. Common use cases are described in the following sections.

Embedding third party web content

Many applications need the ability to load and display third-party content within their user interface. However, when multiple web app owners are involved—a common scenario with embedded applications—it becomes difficult to establish consistent end-to-end policies. For example, security settings can prevent a traditional <iframe> from embedding certain types of content, even when businesses have a legitimate need to do so. Unlike <iframe> elements, Controlled Frames are designed to bypass these restrictions, allowing applications to load and display content even if it explicitly prohibits standard embedding.

Use cases

  • Classroom presentations: A teacher uses a classroom touchscreen to switch between educational resources that would normally block iframe embedding.
  • Digital signage in retail or malls: A shopping mall kiosk cycles through websites from different stores. Controlled Frames ensure these pages load correctly even if they restrict embedding.

Code samples

The following Controlled Frame APIs are helpful for managing embedded content.

Navigation: Controlled Frames provide multiple methods to programmatically manage and control the navigation and navigation history of the embedded content.

The src attribute gets or sets the URL of the content displayed in the frame, functioning the same way as the HTML attribute.

controlledframe.src = "https://example.com";

The back() method navigates back one step in the frame's history. The returned promise resolves to a boolean indicating whether the navigation was successful.

document.getElementById('backBtn').addEventListener('click', () => {
controlledframe.back().then((success) => {
console.log(`Back navigation ${success ? 'succeeded' : 'failed'}`); }).catch((error) => {
   console.error('Error during back navigation:', error);
   });
});

The forward() method navigates forward one step in the frame's history. The returned promise resolves to a boolean indicating whether the navigation was successful.

document.getElementById('forwardBtn').addEventListener('click', () => {
controlledframe.forward().then((success) => {
   console.log(`Forward navigation ${success ? 'succeeded' : 'failed'}`);
}).catch((error) => {
    console.error('Error during forward navigation:', error);
  });
});

The reload() method reloads the current page in the frame.

document.getElementById('reloadBtn').addEventListener('click', () => {
   controlledframe.reload();
});

Additionally, Controlled Frames provide events that let you track the full lifecycle of navigation requests—from initiation and redirects to content loading, completion, or abortion.

  • loadstart: Fired when a navigation begins in the frame.
  • loadcommit: Fired when the navigation request has been processed, and the main document content starts loading.
  • contentload: Fired when the main document and its essential resources have finished loading (similar to DOMContentLoaded).
  • loadstop: Fired when all resources for the page (including subframes, images) have finished loading.
  • loadabort: Fired if a navigation is aborted (for example, by user action or another navigation starting).
  • loadredirect: Fired when a server-side redirect occurs during navigation.
controlledframe.addEventListener('loadstart', (event) => {
   console.log('Navigation started:', event.url);
   // Example: Show loading indicator
 });
controlledframe.addEventListener('loadcommit', (event) => {
   console.log('Navigation committed:', event.url);
 });
controlledframe.addEventListener('contentload', (event) => {
   console.log('Content loaded for:', controlledframe.src);
   // Example: Hide loading indicator, maybe run initial script
 });
controlledframe.addEventListener('loadstop', (event) => {
   console.log('All resources loaded for:', controlledframe.src);
 });
controlledframe.addEventListener('loadabort', (event) => {
   console.warn(`Navigation aborted: ${event.url}, Reason: ${event.detail.reason}`);
 });
controlledframe.addEventListener('loadredirect', (event) => {
   console.log(`Redirect detected: ${event.oldUrl} -> ${event.newUrl}`);
});

You can also monitor and potentially intercept specific interactions or requests initiated by the content loaded within the controlled frame, such as attempts to open dialogs, request permissions, or open new windows.

  • dialog: Fired when the embedded content attempts to open a dialog (alert, confirm, prompt). You receive details and can respond.
  • consolemessage: Fired when a message is logged to the console within the frame.
  • permissionrequest: Fired when the embedded content requests a permission (for example, geolocation and notifications). You receive details and can allow or deny the request.
  • newwindow: Fired when the embedded content attempts to open a new window or tab (for example, with window.open or a link with target="_blank"). You receive details and can handle or block the action.
controlledframe.addEventListener('dialog', (event) => {
   console.log(Dialog opened: Type=${event.messageType}, Message=${event.messageText});
   // You will need to respond, e.g., event.dialog.ok() or .cancel()
 });

controlledframe.addEventListener('consolemessage', (event) => {
   console.log(Frame Console [${event.level}]: ${event.message});
 });

controlledframe.addEventListener('permissionrequest', (event) => {
   console.log(Permission requested: Type=${event.permission});
   // You must respond, e.g., event.request.allow() or .deny()
   console.warn('Permission request needs handling - Denying by default');
   if (event.request && event.request.deny) {
     event.request.deny();
   }
});

controlledframe.addEventListener('newwindow', (event) => {
   console.log(New window requested: URL=${event.targetUrl}, Name=${event.name});
   // Decide how to handle this, e.g., open in a new controlled frame and call event.window.attach(), ignore, or block
   console.warn('New window request needs handling - Blocking by default');
 });

There are also state change events that notify you about changes related to the controlled frame's own rendering state, such as modifications to its dimensions or zoom level.

  • sizechanged: Fired when the dimensions of the frame's content change.
  • zoomchange: Fired when the zoom level of the frame's content changes.
controlledframe.addEventListener('sizechanged', (event) => {
  console.log(Frame size changed: Width=${event.width}, Height=${event.height});
});

controlledframe.addEventListener('zoomchange', (event) => {
  console.log(Frame zoom changed: Factor=${event.newZoomFactor});
});

Storage methods: Controlled Frames offer APIs for managing data stored within a frame's partition.

Use clearData() to remove all stored data, which is especially useful for resetting the frame after a user session or ensuring a clean state. The method returns a Promise that resolves when the operation is complete. Optional configuration options can also be provided:

  • types: An array of strings specifying which types of data to clear (for example, ['cookies', 'localStorage', 'indexedDB']). If omitted, all applicable data types are typically cleared.
  • options: Control the clearing process, such as specifying a time range using a since property (timestamp in milliseconds since epoch) to only clear data created after that time.

Example: Clear all storage associated with the Controlled Frame

function clearAllPartitionData() {
   console.log('Clearing all data for partition:', controlledframe.partition);
   controlledframe.clearData()
     .then(() => {
       console.log('Partition data cleared successfully.');
     })
     .catch((error) => {
       console.error('Error clearing partition data:', error);
     });
}

Example: Clear only cookies and localStorage created in the last hour

function clearRecentCookiesAndStorage() {
   const oneHourAgo = Date.now() - (60 * 60 * 1000);
   const dataTypesArray = ['cookies', 'localStorage'];
   const dataTypesToClearObject = {};
   for (const type of dataTypesArray) {
      dataTypesToClearObject[type] = true;
   }
   const clearOptions = { since: oneHourAgo };
   console.log(`Clearing ${dataTypesArray.join(', ')} since ${new    Date(oneHourAgo).toISOString()}`); controlledframe.clearData(clearOptions, dataTypesToClearObject) .then(() => {
   console.log('Specified partition data cleared successfully.');
}).catch((error) => {
   console.error('Error clearing specified partition data:', error);
});
}

Extend or alter third-party applications

Beyond simple embedding, the Controlled Frames offer mechanisms for the embedding IWA to exert control over the embedded third-party web content. You can execute scripts within the embedded content, intercept network requests, and override default context menus—all in a secure, isolated environment.

Use cases

  • Enforcing branding across third-party sites: Inject custom CSS and JavaScript into embedded websites to apply a unified visual theme.
  • Restrict Navigation and Link Behavior: Intercept or disable certain <a> tag behaviors with script injection.
  • Automate Recovery After Crashes or Inactivity: Monitor embedded content for failure states (for example, blank screen, script errors) and programmatically reload or reset the session after a timeout.

Code samples

Script Injection: Use executeScript() to inject JavaScript into the controlled frame, letting you customize behavior, add overlays, or extract data from embedded third-party pages. You can provide either inline code as a string or reference one or more script files (using relative paths within the IWA package). The method returns a promise that resolves to the result of the script's execution—typically the value of the last statement.

document.getElementById('scriptBtn').addEventListener('click', () => {
   controlledframe.executeScript({
      code: `document.body.style.backgroundColor = 'lightblue';
             document.querySelectorAll('a').forEach(link =>    link.style.pointerEvents = 'none');
             document.title; // Return a value
            `,
      // You can also inject files:
      // files: ['./injected_script.js'],
}) .then((result) => {
   // The result of the last statement in the script is usually returned.
   console.log('Script execution successful. Result (e.g., page title):', result); }).catch((error) => {
   console.error('Script execution failed:', error);
   });
});

Style injection: Use insertCSS() to apply custom styles to pages loaded within a Controlled Frame.

document.getElementById('cssBtn').addEventListener('click', () => {
  controlledframe.insertCSS({
    code: `body { font-family: monospace; }`
    // You can also inject files:
    // files: ['./injected_styles.css']
  })
  .then(() => {
    console.log('CSS injection successful.');
  })
  .catch((error) => {
    console.error('CSS injection failed:', error);
  });
});

Network Request Interception: Use the WebRequest API to observe and potentially modify network requests from the embedded page, such as blocking requests, altering headers, or logging usage.

// Get the request object
const webRequest = controlledframe.request;

// Create an interceptor for a specific URL pattern
const interceptor = webRequest.createWebRequestInterceptor({
  urlPatterns: ["*://evil.com/*"],
  blocking: true,
  includeHeaders: "all"
});

// Add a listener to block the request
interceptor.addEventListener("beforerequest", (event) => {
  console.log('Blocking request to:', event.url);
  event.preventDefault();
});

// Add a listener to modify request headers
interceptor.addEventListener("beforesendheaders", (event) => {
  console.log('Modifying headers for:', event.url);
  const newHeaders = new Headers(event.headers);
  newHeaders.append('X-Custom-Header', 'MyValue');
  event.setRequestHeaders(newHeaders);
});

Adding Custom Context Menus: Use the contextMenus API to add, remove, and handle custom right-click menus within the embedded frame. This example shows how to add a custom "Copy selection" menu inside a Controlled Frame. When text is selected and the user right-clicks, the menu appears. Clicking it copies the selected text to the clipboard, enabling simple, user-friendly interactions within embedded content.

const menuItemProperties = {
  id: "copy-selection",
  title: "Copy selection",
  contexts: ["selection"],
  documentURLPatterns: [new URLPattern({ hostname: '*.example.com'})]
};

// Create the context menu item using a promise
try {
  await controlledframe.contextMenus.create(menuItemProperties);
  console.log(`Context menu item "${menuItemProperties.id}" created successfully.`);
} catch (error) {
  console.error(`Failed to create context menu item:`, error);
}

// Add a standard event listener for the 'click' event
controlledframe.contextMenus.addEventListener('click', (event) => {
    if (event.menuItemId === "copy-selection" && event.selectionText) {
        navigator.clipboard.writeText(event.selectionText)
          .then(() => console.log("Text copied to clipboard."))
          .catch(err => console.error("Failed to copy text:", err));
    }
});

Demo

Check out the Controlled Frame demo for an overview of the methods of Controlled Frames.

The Controlled Frame demo

Alternatively, IWA Kitchen Sink, features an app with multiple tabs, each demonstrating a different IWA API such as Controlled Frames, Direct Sockets, and more.

IWA Kitchen Sink

Conclusion

Controlled Frames offer a powerful and secure way to embed, extend, and interact with third-party web content in Isolated Web Apps (IWAs). By overcoming the limitations of iframes, they enable new capabilities such as executing scripts inside embedded content, intercepting network requests, and implementing custom context menus—all while maintaining strict isolation boundaries. However, because these APIs offer deep control over embedded content, they also come with additional security constraints and are only available within IWAs, which are designed to enforce stronger guarantees for both users and developers. For most use cases, developers should first consider using standard <iframe> elements, which are simpler and sufficient in many scenarios. Controlled Frames should be evaluated when iframe-based solutions are blocked by embedding restrictions or lack the necessary control and interaction capabilities. Whether you're building kiosk experiences, integrating third-party tools, or designing modular plugin systems, Controlled Frames enable fine-grained control in a structured, permissioned, and secure environment—making them a critical tool in the next generation of advanced web applications.

More Resources