Iframes & Embeds
The <iframe> element embeds another HTML document within the current page. It creates a nested browsing context — a完全独立的 document with its own DOM, styles, and scripts. Iframes power everything from YouTube embeds and Google Maps to payment forms and social media widgets. While powerful, they introduce security, performance, and UX considerations that demand careful attention.
Every iframe starts with a src attribute pointing to the embedded document. The browser fetches that URL, parses it, and renders it inside a rectangular region defined by width and height. The embedded document runs in its own origin, which means it cannot access the parent page's DOM unless explicitly allowed.
note
The simplest iframe requires only a src attribute. However, production-ready iframes should always include name, width, height, title, and loading attributes. The name attribute is used as the targeting name for links and forms.
| Attribute | Values | Description |
|---|---|---|
| src | URL | URL of the page to embed |
| name | string | Targetable name for links and forms |
| width / height | pixels | Dimensions of the iframe (use CSS for responsive) |
| loading | lazy | eager | Defer loading until visible (lazy recommended) |
| sandbox | space-separated tokens | Restricts capabilities of the embedded document |
| allow | Feature Policy string | Controls API access (microphone, camera, etc.) |
| referrerpolicy | ReferrerPolicy value | Controls Referer header sent with the request |
| srcdoc | HTML string | Inline HTML content instead of src URL |
| title | string | Accessible label (required for a11y) |
| 1 | <!-- Basic iframe --> |
| 2 | <iframe |
| 3 | src="https://example.com/widget" |
| 4 | width="600" |
| 5 | height="400" |
| 6 | title="Example widget" |
| 7 | ></iframe> |
| 8 | |
| 9 | <!-- With name and lazy loading --> |
| 10 | <iframe |
| 11 | src="https://example.com/map" |
| 12 | name="map-frame" |
| 13 | width="100%" |
| 14 | height="450" |
| 15 | title="Interactive map" |
| 16 | loading="lazy" |
| 17 | ></iframe> |
| 18 | |
| 19 | <!-- Links targeting a named iframe --> |
| 20 | <iframe |
| 21 | src="about:blank" |
| 22 | name="content-frame" |
| 23 | width="800" |
| 24 | height="500" |
| 25 | title="Content viewer" |
| 26 | ></iframe> |
| 27 | |
| 28 | <a href="page1.html" target="content-frame">Page 1</a> |
| 29 | <a href="page2.html" target="content-frame">Page 2</a> |
| 30 | |
| 31 | <!-- Inline content with srcdoc --> |
| 32 | <iframe |
| 33 | srcdoc="<p>Hello from <strong>inside</strong> the iframe!</p>" |
| 34 | width="400" |
| 35 | height="100" |
| 36 | title="Inline content" |
| 37 | sandbox="allow-scripts" |
| 38 | ></iframe> |
Iframes are a prime vector for clickjacking, data exfiltration, and other attacks. The sandbox attribute is your primary defense — it applies a set of restrictions to the embedded content. Without sandbox, an iframe can run scripts, submit forms, open popups, and navigate the parent window.
| Sandbox Token | Allows |
|---|---|
| allow-scripts | JavaScript execution |
| allow-same-origin | Treat content as same-origin (dangerous with allow-scripts) |
| allow-forms | Form submission |
| allow-popups | Opening new windows via window.open |
| allow-popups-to-escape-sandbox | Popups that are not sandboxed |
| allow-top-navigation | Navigate the top-level window |
| allow-top-navigation-by-user-activation | Navigate top window only on user gesture |
| allow-downloads | Trigger file downloads |
| allow-presentation | Start a presentation session |
| allow-orientation-lock | Lock screen orientation |
The sandbox attribute with no value enables all restrictions. You then opt-in to specific capabilities by adding tokens. This "default-deny" approach is the safest pattern:
| 1 | <!-- Maximum security: no sandbox tokens = all restrictions active --> |
| 2 | <iframe |
| 3 | src="https://third-party.com/widget" |
| 4 | sandbox="" |
| 5 | title="Third-party widget" |
| 6 | width="400" |
| 7 | height="300" |
| 8 | ></iframe> |
| 9 | |
| 10 | <!-- Moderate security: scripts allowed, but same-origin blocked --> |
| 11 | <iframe |
| 12 | src="https://third-party.com/form" |
| 13 | sandbox="allow-scripts allow-forms" |
| 14 | title="Contact form" |
| 15 | width="500" |
| 16 | height="400" |
| 17 | ></iframe> |
| 18 | |
| 19 | <!-- Minimal restrictions for trusted content --> |
| 20 | <iframe |
| 21 | src="https://trusted-cdn.com/app" |
| 22 | sandbox="allow-scripts allow-same-origin allow-forms allow-popups" |
| 23 | title="Trusted application" |
| 24 | width="800" |
| 25 | height="600" |
| 26 | ></iframe> |
| 27 | |
| 28 | <!-- Using allow for feature policy --> |
| 29 | <iframe |
| 30 | src="https://app.example.com/video-call" |
| 31 | allow="camera 'src'; microphone 'src'; fullscreen" |
| 32 | sandbox="allow-scripts allow-same-origin" |
| 33 | title="Video call app" |
| 34 | width="640" |
| 35 | height="480" |
| 36 | ></iframe> |
| 37 | |
| 38 | <!-- referrerpolicy controls what Referer header is sent --> |
| 39 | <iframe |
| 40 | src="https://analytics.example.com" |
| 41 | referrerpolicy="no-referrer" |
| 42 | sandbox="allow-scripts" |
| 43 | title="Analytics widget" |
| 44 | width="300" |
| 45 | height="200" |
| 46 | ></iframe> |
danger
The allow attribute (formerly Feature Policy, now Permissions Policy) controls access to browser APIs like camera, microphone, geolocation, and fullscreen:
| 1 | <!-- Grant camera and microphone access --> |
| 2 | <iframe |
| 3 | src="https://meet.example.com/room" |
| 4 | allow="camera *; microphone *" |
| 5 | title="Video conference" |
| 6 | ></iframe> |
| 7 | |
| 8 | <!-- Allow fullscreen only --> |
| 9 | <iframe |
| 10 | src="https://player.example.com/video" |
| 11 | allow="fullscreen" |
| 12 | title="Video player" |
| 13 | ></iframe> |
| 14 | |
| 15 | <!-- Geolocation with self + specific origin --> |
| 16 | <iframe |
| 17 | src="https://maps.example.com" |
| 18 | allow="geolocation 'src' https://trusted-origin.com" |
| 19 | title="Map embed" |
| 20 | ></iframe> |
The postMessage() API enables cross-origin communication between a parent window and an iframe. Both sides must call postMessage and listen for message events. This is the only way for cross-origin iframes to exchange data safely — direct DOM access is blocked by the same-origin policy.
The pattern works in both directions: the parent can send messages to the iframe, and the iframe can send messages to the parent. The origin property on the event object must ◆always be validated before trusting the message data.
| 1 | <!-- Parent page: sending and receiving --> |
| 2 | <iframe |
| 3 | src="https://widget.example.com" |
| 4 | id="widget-frame" |
| 5 | title="Widget" |
| 6 | sandbox="allow-scripts allow-same-origin" |
| 7 | ></iframe> |
| 8 | |
| 9 | <script> |
| 10 | const iframe = document.getElementById('widget-frame'); |
| 11 | |
| 12 | // Send a message to the iframe |
| 13 | iframe.contentWindow.postMessage( |
| 14 | { type: 'CONFIGURE', theme: 'dark', userId: 42 }, |
| 15 | 'https://widget.example.com' |
| 16 | ); |
| 17 | |
| 18 | // Listen for messages from the iframe |
| 19 | window.addEventListener('message', (event) => { |
| 20 | // ALWAYS verify the origin |
| 21 | if (event.origin !== 'https://widget.example.com') return; |
| 22 | |
| 23 | const { type, data } = event.data; |
| 24 | if (type === 'USER_ACTION') { |
| 25 | console.log('Widget reported:', data); |
| 26 | updateUI(data); |
| 27 | } |
| 28 | }); |
| 29 | </script> |
| 30 | |
| 31 | <!-- Inside the iframe (widget.html): --> |
| 32 | <script> |
| 33 | // Listen for messages from the parent |
| 34 | window.addEventListener('message', (event) => { |
| 35 | if (event.origin !== 'https://parent.example.com') return; |
| 36 | |
| 37 | const { type, theme, userId } = event.data; |
| 38 | if (type === 'CONFIGURE') { |
| 39 | applyTheme(theme); |
| 40 | loadUserData(userId); |
| 41 | } |
| 42 | }); |
| 43 | |
| 44 | // Send results back to the parent |
| 45 | function notifyParent(action) { |
| 46 | window.parent.postMessage( |
| 47 | { type: 'USER_ACTION', data: action }, |
| 48 | 'https://parent.example.com' |
| 49 | ); |
| 50 | } |
| 51 | </script> |
warning
Third-party services provide embed codes that use iframes under the hood. Each service has specific requirements for sandbox attributes, allow policies, and URL parameters. Here are the most common embed patterns:
| 1 | <!-- YouTube video embed --> |
| 2 | <iframe |
| 3 | width="560" |
| 4 | height="315" |
| 5 | src="https://www.youtube.com/embed/dQw4w9WgXcQ" |
| 6 | title="YouTube video player" |
| 7 | allow="accelerometer; autoplay; clipboard-write; |
| 8 | encrypted-media; gyroscope; picture-in-picture" |
| 9 | referrerpolicy="strict-origin-when-cross-origin" |
| 10 | sandbox="allow-scripts allow-same-origin allow-popups" |
| 11 | ></iframe> |
| 12 | |
| 13 | <!-- Google Maps embed --> |
| 14 | <iframe |
| 15 | width="600" |
| 16 | height="450" |
| 17 | src="https://www.google.com/maps/embed?pb=!1m18..." |
| 18 | title="Google Maps location" |
| 19 | allow="geolocation" |
| 20 | sandbox="allow-scripts allow-same-origin" |
| 21 | loading="lazy" |
| 22 | ></iframe> |
| 23 | |
| 24 | <!-- CodePen embed --> |
| 25 | <iframe |
| 26 | height="400" |
| 27 | src="https://codepen.io/username/embed/preview/abc123" |
| 28 | title="CodePen example" |
| 29 | sandbox="allow-scripts allow-same-origin allow-forms" |
| 30 | loading="lazy" |
| 31 | ></iframe> |
| 32 | |
| 33 | <!-- Spotify embed --> |
| 34 | <iframe |
| 35 | src="https://open.spotify.com/embed/track/4cOdK2wGLETKBW3PvgPWqT" |
| 36 | width="300" |
| 37 | height="380" |
| 38 | title="Spotify track embed" |
| 39 | allow="autoplay; encrypted-media" |
| 40 | sandbox="allow-scripts allow-same-origin" |
| 41 | loading="lazy" |
| 42 | ></iframe> |
| 43 | |
| 44 | <!-- Vimeo embed --> |
| 45 | <iframe |
| 46 | src="https://player.vimeo.com/video/123456789" |
| 47 | width="640" |
| 48 | height="360" |
| 49 | title="Vimeo video player" |
| 50 | allow="autoplay; fullscreen; picture-in-picture" |
| 51 | sandbox="allow-scripts allow-same-origin allow-popups" |
| 52 | loading="lazy" |
| 53 | ></iframe> |
info
The loading attribute defers iframe loading until the iframe approaches the viewport. With loading="lazy", the browser does not fetch the iframe source until the user scrolls near it. This can dramatically reduce initial page weight and improve performance, especially for pages with multiple embeds.
| Value | Behavior | Use Case |
|---|---|---|
| lazy | Defers loading until near viewport | Embeds below the fold |
| eager | Loads immediately (default) | Above-fold critical embeds |
The browser typically uses a distance threshold of 3000-8000 pixels from the viewport. This means the iframe starts loading before the user reaches it, ensuring it is ready when scrolled into view. The exact threshold varies by browser and platform.
| 1 | <!-- Lazy-loaded iframe (below the fold) --> |
| 2 | <iframe |
| 3 | src="https://example.com/heavy-widget" |
| 4 | width="600" |
| 5 | height="400" |
| 6 | title="Heavy widget" |
| 7 | loading="lazy" |
| 8 | sandbox="allow-scripts" |
| 9 | ></iframe> |
| 10 | |
| 11 | <!-- Eager iframe (above the fold — default) --> |
| 12 | <iframe |
| 13 | src="https://example.com/critical-embed" |
| 14 | width="600" |
| 15 | height="400" |
| 16 | title="Critical embed" |
| 17 | loading="eager" |
| 18 | sandbox="allow-scripts" |
| 19 | ></iframe> |
| 20 | |
| 21 | <!-- Content-visibility CSS for additional deferred rendering --> |
| 22 | <div style="content-visibility: auto; contain-intrinsic-size: 500px;"> |
| 23 | <iframe |
| 24 | src="https://example.com/far-down-page" |
| 25 | width="600" |
| 26 | height="400" |
| 27 | title="Far down page" |
| 28 | loading="lazy" |
| 29 | sandbox="allow-scripts" |
| 30 | ></iframe> |
| 31 | </div> |
best practice
Native iframes have fixed dimensions set via width and height attributes. To make them fluid, use the CSS aspect-ratio property. This maintains the correct proportions across all screen sizes without JavaScript.
The classic "padding-bottom" hack is no longer needed — modern browsers support aspect-ratio universally. Combine it with width: 100% and height: auto for a fully responsive embed.
| 1 | <!-- Responsive iframe container --> |
| 2 | <style> |
| 3 | .embed-container { |
| 4 | width: 100%; |
| 5 | max-width: 800px; |
| 6 | margin: 0 auto; |
| 7 | } |
| 8 | |
| 9 | .responsive-iframe { |
| 10 | width: 100%; |
| 11 | height: auto; |
| 12 | aspect-ratio: 16 / 9; |
| 13 | border: none; |
| 14 | } |
| 15 | |
| 16 | .video-wrapper { |
| 17 | width: 100%; |
| 18 | aspect-ratio: 16 / 9; |
| 19 | background: #000; |
| 20 | border-radius: 8px; |
| 21 | overflow: hidden; |
| 22 | } |
| 23 | |
| 24 | .video-wrapper iframe { |
| 25 | width: 100%; |
| 26 | height: 100%; |
| 27 | border: none; |
| 28 | } |
| 29 | |
| 30 | /* Different aspect ratios */ |
| 31 | .square-frame { |
| 32 | aspect-ratio: 1 / 1; |
| 33 | } |
| 34 | |
| 35 | .wide-frame { |
| 36 | aspect-ratio: 21 / 9; |
| 37 | } |
| 38 | |
| 39 | .portrait-frame { |
| 40 | aspect-ratio: 9 / 16; |
| 41 | } |
| 42 | </style> |
| 43 | |
| 44 | <!-- 16:9 responsive YouTube embed --> |
| 45 | <div class="embed-container"> |
| 46 | <div class="video-wrapper"> |
| 47 | <iframe |
| 48 | src="https://www.youtube.com/embed/dQw4w9WgXcQ" |
| 49 | title="Responsive video" |
| 50 | allow="autoplay; fullscreen" |
| 51 | sandbox="allow-scripts allow-same-origin allow-popups" |
| 52 | loading="lazy" |
| 53 | ></iframe> |
| 54 | </div> |
| 55 | </div> |
| 56 | |
| 57 | <!-- Square responsive embed --> |
| 58 | <div class="embed-container"> |
| 59 | <div class="video-wrapper square-frame"> |
| 60 | <iframe |
| 61 | src="https://example.com/square-widget" |
| 62 | title="Square widget" |
| 63 | sandbox="allow-scripts" |
| 64 | loading="lazy" |
| 65 | ></iframe> |
| 66 | </div> |
| 67 | </div> |
Live preview of a responsive video embed container:
The title attribute on an iframe provides the accessible name that screen readers announce when entering the iframe. This is the single most important accessibility requirement for iframes — without it, screen reader users hear only "frame" or "object" with no context.
Iframes that contain only decorative or hidden content should use aria-hidden="true" to remove them from the accessibility tree. However, title is still required for valid HTML and should describe the iframe's purpose even when hidden.
| 1 | <!-- Accessible iframe (always include title!) --> |
| 2 | <iframe |
| 3 | src="https://example.com/widget" |
| 4 | title="Stock price ticker — updated in real time" |
| 5 | width="400" |
| 6 | height="200" |
| 7 | sandbox="allow-scripts" |
| 8 | ></iframe> |
| 9 | |
| 10 | <!-- Decorative/hidden iframe — hide from screen readers --> |
| 11 | <iframe |
| 12 | src="https://example.com/tracking-pixel" |
| 13 | title="Analytics iframe" |
| 14 | aria-hidden="true" |
| 15 | tabindex="-1" |
| 16 | width="1" |
| 17 | height="1" |
| 18 | sandbox="allow-scripts" |
| 19 | ></iframe> |
| 20 | |
| 21 | <!-- Iframe that presents a form --> |
| 22 | <iframe |
| 23 | src="https://example.com/checkout" |
| 24 | title="Secure payment form for completing your purchase" |
| 25 | width="600" |
| 26 | height="500" |
| 27 | sandbox="allow-scripts allow-forms allow-same-origin" |
| 28 | allow="payment" |
| 29 | ></iframe> |
| 30 | |
| 31 | <!-- Empty/placeholder iframe --> |
| 32 | <iframe |
| 33 | src="about:blank" |
| 34 | title="Content area — navigation links will load pages here" |
| 35 | name="content-area" |
| 36 | width="100%" |
| 37 | height="500" |
| 38 | ></iframe> |
best practice
Iframe Decision Guide
Security Checklist
warning
Example of a hardened production iframe configuration:
| 1 | <!-- Production-hardened iframe --> |
| 2 | <iframe |
| 3 | src="https://trusted-cdn.example.com/widget" |
| 4 | title="Weather widget — weekly forecast" |
| 5 | sandbox="allow-scripts" |
| 6 | allow="" |
| 7 | referrerpolicy="no-referrer" |
| 8 | loading="lazy" |
| 9 | width="100%" |
| 10 | height="auto" |
| 11 | style="aspect-ratio: 4 / 3; border: none; max-width: 400px;" |
| 12 | ></iframe> |