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 withtarget="_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.
Alternatively, IWA Kitchen Sink, features an app with multiple tabs, each demonstrating a different IWA API such as Controlled Frames, Direct Sockets, and more.
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.