Introduction to the Fenced Frames API
The Fenced Frames API is a privacy-preserving web platform feature designed to isolate embedded content from its embedding context. It provides a secure container—a "fenced frame"—that prevents cross-site data leakage while still enabling useful embedded experiences like privacy-safe advertising, secure payment forms, and partitioned third-party content. Unlike traditional iframes, fenced frames enforce strict communication boundaries, ensuring that the embedder cannot access sensitive cross-site data from the embedded content and vice versa.
This guide covers everything you need to know: the core concepts, practical implementation, communication patterns, and best practices for deploying fenced frames in production.
What is a Fenced Frame?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →A fenced frame is a new HTML element (<fencedframe>) that functions similarly to an iframe but with crucial privacy-oriented restrictions. It creates a top-level browsing context that is partitioned from its embedder, preventing the sharing of storage, cookies, and network state across sites. The frame appears visually identical to an iframe but operates under a completely separate security model.
Key characteristics of fenced frames include:
- Storage Partitioning: All storage APIs (cookies, localStorage, IndexedDB) are isolated to the fenced frame's origin and cannot be accessed by the embedder.
- Network Partitioning: Network requests from within the fenced frame use a separate network partition, preventing cross-site tracking via cache or connection reuse.
- Restricted Communication: The embedder cannot directly access the fenced frame's DOM, post messages freely, or read its attributes beyond what the API explicitly allows.
- Opaque URLs: The embedder cannot observe the exact URL loaded in the fenced frame, only an opaque source representation.
The Privacy Problem It Solves
Traditional iframes allow embedded third-party content to access unpartitioned storage and cookies, which has historically enabled cross-site tracking. Ad tech platforms could embed an iframe from a tracking domain, read a user's identifier from a third-party cookie, and join that data with first-party information from the embedder. Fenced frames eliminate this capability by physically separating storage and network state, making such data joining impossible without explicit user consent or privacy-preserving aggregation mechanisms.
Why Fenced Frames Matter
The Fenced Frames API is a cornerstone of several major privacy initiatives on the web, including:
- Private Advertising: Enables interest-group-based ad targeting (via the Protected Audience API, formerly FLEDGE) without exposing individual user data to advertisers or publishers.
- Shared Storage API: Provides a secure execution environment for cross-site data that can only be read within the fenced frame, never by the embedder.
- Third-Party Cookie Deprecation: Replaces functionality previously reliant on third-party cookies with privacy-respecting alternatives that maintain utility for embedded services.
- Cross-Site Isolation Without Opt-Ins: Achieves strong isolation boundaries without requiring servers to send
Cross-Origin-Opener-PolicyorCross-Origin-Embedder-Policyheaders.
For developers, fenced frames represent a fundamental shift in how embedded content is structured. They require rethinking cross-frame communication patterns and data sharing, moving from unrestricted postMessage channels to controlled, auditable data flows.
Getting Started: Creating a Fenced Frame
The <fencedframe> Element
The primary way to create a fenced frame is through the dedicated HTML element. Here's the simplest example:
<fencedframe src="https://example.com/embedded-content.html"></fencedframe>
At first glance, this looks identical to an iframe. However, the behavior diverges significantly once the frame loads. The embedder page cannot inspect the fenced frame's src attribute after creation—it becomes opaque. Attempts to read fencedframe.src will return the empty string or an opaque source value, depending on the browser implementation.
Setting Attributes
The fenced frame supports a subset of iframe attributes, with some important differences:
<fencedframe
src="https://example.com/content.html"
width="300"
height="250"
sandbox="allow-scripts"
allow="payment"
></fencedframe>
widthandheight: Work as expected for visual sizing.sandbox: Onlyallow-scriptsis supported. Other sandbox flags likeallow-same-originorallow-popupsare not permitted because they could break isolation.allow: Feature policy permissions can be delegated, but only for a restricted set of features that don't compromise privacy (e.g.,payment,camerawith user activation).src: Must be a valid URL. Once set and loaded, it becomes opaque to the embedder.
Programmatic Creation via JavaScript
You can also create fenced frames dynamically using JavaScript:
const fencedFrame = document.createElement('fencedframe');
fencedFrame.src = 'https://example.com/widget.html';
fencedFrame.width = '300';
fencedFrame.height = '250';
fencedFrame.sandbox = 'allow-scripts';
document.body.appendChild(fencedFrame);
// After appending, src becomes opaque
console.log(fencedFrame.src); // Outputs: '' or opaque source
Once the fenced frame is appended to the DOM and begins loading, its src attribute becomes opaque. You cannot change the src of an already-loaded fenced frame—any attempt to set it again will be silently ignored.
Communication with Fenced Frames
Understanding Opaque Sources
When you set the src of a fenced frame, the browser internally registers the URL but does not expose it to the embedder's JavaScript. This prevents the embedder from inferring user data based on which URL was loaded. Instead, the embedder works with an opaque source object when needed:
// The embedder can obtain an opaque source object
const opaqueSource = fencedFrame.srcObject;
// opaqueSource is not a URL string—it's an opaque handle
// You can use this opaque source with certain APIs like Shared Storage
// but cannot extract the underlying URL from it
postMessage Restrictions
Traditional postMessage between embedder and fenced frame is severely restricted. By default, fenced frames cannot send messages to their embedder, and the embedder cannot send messages into the fenced frame. This prevents data exfiltration through messaging channels.
However, there are controlled mechanisms for specific use cases:
Using FencedFrameConfig and Shared Storage
The primary way to pass data into a fenced frame is through the Shared Storage API combined with a FencedFrameConfig object. This allows the embedder to load a URL determined by privacy-preserving operations on cross-site data, without ever seeing the URL itself:
// In the embedder's JavaScript
async function loadPrivacySafeAd() {
// Run a Shared Storage operation to select a URL
const config = await window.sharedStorage.selectURL(
'ad-selection',
[
{ url: 'https://advertiser.example/ad1.html' },
{ url: 'https://advertiser.example/ad2.html' },
{ url: 'https://advertiser.example/ad3.html' }
],
{ data: { campaignId: 'spring-sale' } }
);
// Create a fenced frame with the resulting config
const frame = document.createElement('fencedframe');
frame.config = config; // Use config, not src
document.body.appendChild(frame);
// The embedder never knows which ad URL was selected
console.log(frame.src); // Opaque
}
The selectURL operation executes inside a restricted Shared Storage worklet, where cross-site data (like ad interest groups) can be processed. The worklet returns an opaque URL configuration that is fed directly into the fenced frame. The embedder never gains access to the raw URL, preserving user privacy.
Shared Storage Worklet Example
Here's what the worklet code looks like for the above example:
// ad-selection-worklet.js
class AdSelector {
async selectURL(urls, data) {
// Access cross-site shared storage
const interestGroup = await sharedStorage.get('interest-group');
const budget = await sharedStorage.get('campaign-budget');
// Run privacy-preserving selection logic
let selectedIndex = 0;
if (interestGroup === 'sports' && data.campaignId === 'spring-sale') {
selectedIndex = 1; // sports-focused ad
} else if (budget > 1000) {
selectedIndex = 2; // premium ad
}
return selectedIndex; // Returns index into urls array
}
}
register('ad-selection', AdSelector);
The worklet runs in a sandboxed environment with no access to the embedder's DOM or network state. It can only read from the partitioned Shared Storage and return a URL index.
Reporting from Fenced Frames
While direct communication is blocked, fenced frames can send reports through privacy-preserving channels. The reportEvent API allows the fenced frame to trigger predefined reports to designated endpoints:
// Inside the fenced frame's JavaScript
async function reportConversion() {
// Send a private conversion report
await window.fenced.reportEvent({
eventType: 'conversion',
eventData: { value: 'purchase', amount: 49.99 },
destination: ['https://advertiser.example/reporting']
});
}
The embedder can also initiate event reporting from the fenced frame using FencedFrameConfig with pre-registered reporting URLs:
// Embedder registers reporting during config creation
const config = await window.sharedStorage.selectURL(
'ad-selection',
urls,
{
data: { campaignId: 'spring-sale' },
reportingMetadata: {
'click': 'https://analytics.example/report-click',
'impression': 'https://analytics.example/report-impression'
}
}
);
// Later, trigger a report
await fencedFrame.reportEvent('click');
Practical Use Cases
1. Privacy-Preserving Ad Rendering
The most prominent use case for fenced frames is rendering advertisements selected through the Protected Audience API. Here's a complete workflow:
// Step 1: Embedder page sets up ad slot
async function renderProtectedAd(adSlot) {
// Request an ad from the Protected Audience API
const adConfig = await navigator.runAdAuction({
seller: 'https://publisher.example',
decisionLogicURL: 'https://publisher.example/decision-logic.js',
interestGroupBuyers: [
'https://advertiser1.example',
'https://advertiser2.example'
],
auctionSignals: { placement: 'sidebar' }
});
// Step 2: Render the winning ad in a fenced frame
const frame = document.createElement('fencedframe');
frame.config = adConfig; // Opaque config from auction
frame.width = '300';
frame.height = '250';
adSlot.appendChild(frame);
// Step 3: The fenced frame loads the winning ad creative
// The embedder never knows which ad won or what URL loads
}
2. Secure Payment Widgets
Fenced frames can host payment forms that are completely isolated from the merchant page, preventing the merchant from intercepting payment credentials:
<fencedframe
src="https://payment-provider.example/checkout-widget"
width="100%"
height="400"
sandbox="allow-scripts"
allow="payment"
></fencedframe>
The payment provider's widget runs in its own storage partition, keeping cookies and session data separate. The merchant page cannot read form inputs or intercept the payment flow. After successful payment, the widget can use a limited postMessage channel (if configured) to notify the merchant of completion, but without exposing sensitive details.
3. Embedded Analytics with Event-Level Reporting
Analytics providers can use fenced frames to collect performance metrics without being able to track users across sites:
// Embedder creates analytics fenced frame
async function setupAnalytics() {
const analyticsConfig = await window.sharedStorage.selectURL(
'analytics-config',
[{ url: 'https://analytics.example/collector.html' }],
{ data: { pageType: 'product', category: 'electronics' } }
);
const frame = document.createElement('fencedframe');
frame.config = analyticsConfig;
frame.style.display = 'none'; // Hidden analytics frame
document.body.appendChild(frame);
// Register reporting events
frame.reportEvent('page-view', {
destination: ['https://analytics.example/page-views']
});
}
Best Practices
1. Design for Opaque Communication
Accept that you cannot directly communicate with fenced frame content. Structure your application to use the Shared Storage API and event-level reporting for all cross-boundary data flows. Pre-register all necessary reporting endpoints and metadata before creating the fenced frame.
2. Handle Frame Lifecycle Carefully
Fenced frames have limited lifecycle hooks. You can observe when a frame is loaded via the load event on the <fencedframe> element, but you cannot access the internal document's ready state. Use this sparingly:
const frame = document.createElement('fencedframe');
frame.config = selectedConfig;
frame.addEventListener('load', () => {
// Frame loaded, but we still can't access its internals
// Good time to trigger registered reports if needed
console.log('Fenced frame finished loading');
});
document.body.appendChild(frame);
3. Always Use config, Not src, for Dynamic URLs
When loading URLs determined by privacy-preserving operations (Shared Storage selectURL, Protected Audience auctions), always use the config property rather than src. Setting src directly bypasses the privacy protections and may expose raw URLs:
// CORRECT: Use config from privacy-preserving API
frame.config = await window.sharedStorage.selectURL(/*...*/);
// INCORRECT: Extracting and setting src manually
// This defeats the purpose and may not even work
const url = somehowExtractURL(config); // Not possible by design
frame.src = url; // Avoid this pattern
4. Prefer Declarative APIs When Possible
For static embedded content that doesn't require dynamic URL selection, use declarative <fencedframe> elements in your HTML. This improves performance and simplifies your code:
<!-- Good for static, trusted embedded content -->
<fencedframe
src="https://trusted-cdn.example/static-widget.html"
width="300"
height="250"
sandbox="allow-scripts"
></fencedframe>
5. Plan for Graceful Degradation
Not all browsers support fenced frames yet. Implement feature detection and provide fallback experiences:
async function createAdFrame(adSlot) {
if ('HTMLFencedFrameElement' in window) {
// Use fenced frame with full privacy protections
const config = await runPrivacyPreservingAdAuction();
const frame = document.createElement('fencedframe');
frame.config = config;
frame.width = '300';
frame.height = '250';
adSlot.appendChild(frame);
} else {
// Fallback: Use iframe with server-side ad decision
const iframe = document.createElement('iframe');
iframe.src = 'https://ad-server.example/fallback-ad?slot=sidebar';
iframe.width = '300';
iframe.height = '250';
adSlot.appendChild(iframe);
}
}
6. Keep Fenced Frame Content Lightweight
Fenced frames run with restricted resources and cannot rely on shared caches or pre-warmed connections. Optimize the embedded content for fast, standalone loading:
- Minimize dependency on external resources
- Preload critical assets within the fenced frame's origin
- Use efficient rendering paths (no heavy frameworks if avoidable)
- Keep JavaScript bundles small and focused
7. Audit Reporting Destinations
All reporting from fenced frames must go through pre-registered endpoints. Carefully audit these destinations to ensure they don't inadvertently leak cross-site data. Reports should contain only aggregate or event-level data, never user-specific identifiers:
// Inside fenced frame: Good reporting practice
window.fenced.reportEvent({
eventType: 'conversion',
eventData: {
// Aggregate data only
productCategory: 'electronics',
priceRange: '50-100'
// NO user IDs, device fingerprints, or raw PII
},
destination: ['https://audited-reporting.example/endpoint']
});
Security Considerations
Sandbox Limitations
Fenced frames automatically enforce strong sandboxing. Only allow-scripts is available as a sandbox attribute. This means fenced frames cannot:
- Open popups or new windows
- Navigate the top-level window
- Access the embedder's DOM in any way
- Use plugins or non-script content
Network Partitioning
All network requests from within a fenced frame use a dedicated network partition. This means:
- The fenced frame's HTTP cache is separate from the embedder's cache
- DNS cache and connection pools are isolated
- TLS session resumption does not cross the fenced frame boundary
- Even if the same origin is loaded in a fenced frame and a regular iframe, they do not share network state
Opaque Source Enforcement
The browser enforces source opacity at the engine level. Even with DevTools open, the embedder's JavaScript cannot extract the fenced frame's URL. This is a hard security boundary, not a soft convention:
// All of these return opaque or empty values:
console.log(fencedFrame.src); // '' or opaque source
console.log(fencedFrame.getAttribute('src')); // '' or opaque source
console.log(fencedFrame.contentWindow?.location.href); // Error or undefined
// The only way to interact is through the approved APIs:
// - Shared Storage selectURL for URL selection
// - reportEvent for outbound reporting
// - config property for opaque configuration
Browser Support and Feature Detection
As of 2024, fenced frames are available in Chrome and Chromium-based browsers with the appropriate feature flags enabled. Other browsers are evaluating the specification. Here's a comprehensive feature detection approach:
// Check for fenced frame support
const fencedFrameSupport = {
elementExists: 'HTMLFencedFrameElement' in window,
sharedStorageAvailable: 'sharedStorage' in window,
protectedAudienceAvailable: 'runAdAuction' in navigator,
isFullySupported() {
return this.elementExists &&
this.sharedStorageAvailable &&
this.protectedAudienceAvailable;
},
getSupportLevel() {
if (this.isFullySupported()) return 'full';
if (this.elementExists) return 'partial';
return 'none';
}
};
console.log('Fenced frame support:', fencedFrameSupport.getSupportLevel());
For production deployments, always test in browser versions that fully support the APIs you depend on. Use origin trials or feature flags during development to access cutting-edge functionality.
Debugging Fenced Frames
Debugging fenced frames requires different techniques than traditional iframes because the embedder cannot inspect the frame's internals. Here are strategies for effective debugging:
- Use Chrome DevTools: Open the fenced frame's context by clicking on it in the DevTools iframe list. This gives you a separate DevTools instance for the fenced frame.
- Enable DevTools persistence: Configure DevTools to preserve logs across frame contexts to track events flowing between embedder and fenced frame.
- Server-side logging: Since the embedder cannot access the fenced frame's console, implement robust server-side logging for fenced frame code.
- Report-based debugging: Use the reporting infrastructure to send debug information through the approved channels during development.
// Debug helper inside fenced frame
async function debugLog(message, data) {
// In development, send structured debug reports
if (location.hostname === 'localhost') {
await window.fenced.reportEvent({
eventType: 'debug',
eventData: { message, data, timestamp: Date.now() },
destination: ['https://your-dev-server.example/debug-endpoint']
});
}
}
Conclusion
The Fenced Frames API represents a fundamental evolution in how the web platform handles embedded content and cross-site data. By enforcing strict isolation boundaries, opaque sources, and controlled communication channels, it enables privacy-preserving use cases that were previously impossible without relying on deprecated mechanisms like third-party cookies.
For developers, the key takeaways are: embrace opaque configurations over direct URL manipulation, design data flows around Shared Storage and event-level reporting, and plan for a world where cross-frame communication is the exception rather than the rule. While the API introduces new constraints, it also opens up powerful capabilities for building trustworthy, privacy-respecting web experiences that maintain utility for both users and service providers.
As browser support expands and the ecosystem matures, fenced frames will become an essential tool for any developer working with embedded content, advertising, payments, or cross-site integrations. Start experimenting today with origin trials and feature flags to prepare your applications for this privacy-first future.