← Back to DevBytes

Fenced Frames API: Complete Guide

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:

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:

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>

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:

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:

Network Partitioning

All network requests from within a fenced frame use a dedicated network partition. This means:

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:

// 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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles