IframeATM
An embedded page that fits where it lives - the width of its container and a height from a ratio, from the container or from the framed page's own content - with a message bridge in both directions, so the framed page can control the page around it. Built on the native <iframe> and postMessage.
On this page (9)
§Fit by ratio
The default: as wide as its container, 16:9 tall (--iframe-ratio). The framed page here is a srcdoc; a src works the same. Switch the preview to Phone - the frame keeps its ratio at every width.
§Fill the container
data-fit='container': the frame takes the height of the element it lives in - here a card body of 14rem. Give that element a height; the frame never shrinks below --iframe-min-height.
§Follow the content
data-fit='content': the frame is as tall as the framed page; add lines inside and it grows - iframe-resize reports each height. A same-origin page is measured by the host. This one reports its own height with the size message, as a page from another origin must: the docs run every example in a sandbox, where each frame is an origin of its own.
§The framed page controls the host
The buttons live INSIDE the frame: each posts { type: 'defuss:iframe', kind: 'message', name: 'select' } to its parent, the host's .iframe fires iframe-message, and the host sets its own badge and statistic. The host's button posts back into the frame (df$.shadcn.iframe.post) - the framed page switches its own theme. Messages from any other window or origin are ignored.
§Origins, CSP and sites that refuse to be framed
The examples above frame srcdoc pages, which share this page's origin - so the host can measure them and their messages carry its own origin. Across origins, three things decide what works:
- The embedded site decides whether it may be framed at all.
X-Frame-Options: DENYorSAMEORIGIN, or a CSPframe-ancestorswithout your origin, makes the browser refuse the page and show its own error inside the frame - and no script on either side can detect it. Hugging Face model pages sendX-Frame-Options: DENY; the Redeschrift paper therefore frames a model card of its own and links to the original. - Your page's CSP must allow the frame's origin in
frame-src. Messages need no CSP permission, but the framed page must be allowed to run scripts to send any. - The bridge accepts a message only from the window of a frame this element hosts, and only from this page's origin or one listed in
data-origins(nullfor a sandboxed srcdoc). Inside the framed page, setdata-iframe-parentto the host's origin so nothing it sends goes elsewhere; never combineallow-scriptswithallow-same-origininsandboxfor content you do not trust.
<!-- a site that refuses to be framed: caption it with the way out --><figure class="iframe"> <iframe class="iframe-frame" src="https://huggingface.co/kyr0/redeschrift-1.0" title="Redeschrift 1.0 model card"></iframe> <figcaption class="iframe-caption">huggingface.co sends X-Frame-Options: DENY - <a href="https://huggingface.co/kyr0/redeschrift-1.0" target="_blank" rel="noopener">open the model card</a>.</figcaption></figure>§States
Named states via the shared State API, driven per instance through the bound api:
default- not loaded yet: the placeholder shimmers (stopped under reduced motion)loaded- the framed page loaded:data-loadedon the figure (set by the frame's load event)
The first example carries data-state-demo; bun run screenshots drives it via el.api.setState(name) and captures screenshots/{mode}/iframe-{state}.png.
Machine contract - verified against iframe.schema.json by bun run verify:
| State | Type | Values | Default | Description |
|---|---|---|---|---|
loaded | boolean | true, false | false | The framed page loaded (data-loaded on the figure). |
§API
Generated from iframe.ts and the shared State API - the descriptions are their JSDoc, the types are checked by the compiler.
States
type IframeState = 'default' | 'loaded' - setState(name, config) takes the config of the state it names.
| State | Description |
|---|---|
default | Not loaded yet - the frame shows its placeholder shimmer. No config. |
loaded | The framed page loaded (set by the frame's load event, or by setState). No config. |
Every element
| Member | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
el.api.setState<S extends IframeState>(name: S, config?: IframeStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | |||||||||
el.api.getState(): { name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed. Returns | |||||||||
el.api.render(state?: { name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | |||||||||
el.api.settled(): Promise<void> | Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting). Returns | |||||||||
el.store: Store<{ name: IframeState; config: IframeStateConfigs[IframeState] }> | A defuss-store store of the element's state - subscribe to follow every change (also the user's), set it to drive the component. |
Registry
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
df$.shadcn.iframeApi.setState<S extends IframeState>(el: HTMLElement, name: S, config?: IframeStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | ||||||||||||
df$.shadcn.iframeApi.getState(el: HTMLElement): { name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed.
Returns | ||||||||||||
df$.shadcn.iframeApi.render(state: { name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | ||||||||||||
df$.shadcn.iframeApi.store(el: HTMLElement): Store<{ name: IframeState; config: IframeStateConfigs[IframeState] }> | The element's store (bindComponent made it).
Returns | ||||||||||||
df$.shadcn.iframeApi.commit<S extends IframeState>(el: HTMLElement, name: S, config?: IframeStateConfigs[S]): void | Record a state the element reached on its own (no DOM work) - for a component's own handlers.
| ||||||||||||
df$.shadcn.iframeStates: IframeState[] | The declared states, 'default' first: default, loaded. |
df$.shadcn.iframe
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
post(target: string | HTMLElement, name: string, detail?: unknown): boolean | Post a named message into a framed page; it arrives there as an iframe-message event on the document (or as a plain message event carrying { type: 'defuss:iframe', kind: 'message', name, detail }).
Returns | ||||||||||||
send(name: string, detail?: unknown): boolean | Post a named message to the host page - called from inside a framed page; the host's .iframe element fires iframe-message. Set data-iframe-parent on the framed page's html element to the host origin to post to it only.
Returns | ||||||||||||
resize(target: string | HTMLElement): boolean | Measure a same-origin frame's content again (data-fit="content").
Returns |
Events
| Event | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
iframe-message | Fires on the framed page's document when its host page posts a message (df$.shadcn.iframe.post) - the name, the payload and the host's origin.
| ||||||||||||
iframe-resize | Fires when the framed page's content height changes (data-fit="content") - the new height.
|
Types
| Type | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
IframeMessageDetail | What an iframe-message event carries.
| ||||||||||||
IframeResizeDetail | What an iframe-resize event carries.
|
§CSS view file
The three fits, the bare variant and the loading shimmer.
/* -- Iframe component ----------------------------------------- *//* An embedded page as wide as its container; its height from a *//* ratio (default), the container (data-fit="container") or the *//* framed page's content (data-fit="content", --iframe-height *//* written by iframe.js). A shimmer marks it until it loads. */@layer components { .iframe { --iframe-ratio: 16 / 9; --iframe-min-height: 8rem; --iframe-border-width: 1px; display: grid; gap: 0.5rem; margin: 0; inline-size: 100%; min-inline-size: 0; /* border-box: a fit sizes the whole frame, border included */ & > .iframe-frame { display: block; box-sizing: border-box; inline-size: 100%; aspect-ratio: var(--iframe-ratio); border: var(--iframe-border-width) solid var(--border); border-radius: var(--radius-lg); background-color: color-mix(in oklab, var(--muted) 60%, var(--background)); } /* fill the element it lives in - give that element a height; a caption takes an implicit row of its own height */ &[data-fit="container"] { block-size: 100%; grid-template-rows: minmax(0, 1fr); & > .iframe-frame { aspect-ratio: auto; block-size: 100%; min-block-size: var(--iframe-min-height); } } /* follow the framed page's own height: the content plus the border */ &[data-fit="content"] > .iframe-frame { aspect-ratio: auto; block-size: calc(var(--iframe-height, var(--iframe-min-height)) + 2 * var(--iframe-border-width)); } &[data-variant="bare"] { --iframe-border-width: 0px; & > .iframe-frame { border-radius: 0; background-color: transparent; } } } .iframe-caption { color: var(--muted-foreground); font-size: 0.8125rem; text-wrap: pretty; } /* motion only where the reader allows it */ @media (prefers-reduced-motion: no-preference) { .iframe:not([data-loaded]) > .iframe-frame { background-image: linear-gradient(90deg, transparent, color-mix(in oklab, var(--foreground) 7%, transparent), transparent); background-size: 200% 100%; animation: iframe-shimmer 1.4s linear infinite; } .iframe[data-fit="content"] > .iframe-frame { transition: block-size 0.2s ease; } } @keyframes iframe-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } @media (prefers-contrast: more) { .iframe > .iframe-frame { border-color: var(--foreground); } } @media (forced-colors: active) { .iframe > .iframe-frame { border-color: CanvasText; } }}§JavaScript view file
Load state, content-height following and the checked message bridge.
/* -- Iframe component ---------------------------------------- *//* An embedded page that fits where it lives: the width of its *//* container, a height from an aspect ratio, the container's *//* height or the framed page's own content - and a message bridge *//* in both directions over postMessage, checked by origin and by *//* window. Named-state API bound per figure (AGENTS.md "State *//* API"). *//* VERIFIED: (iframe.e2e) fits, both message directions, origin *//* rejection, the hello handshake and the render contract. */// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js —// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.import { defussGlobals, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';const df$ = defussGlobals();const dfDollar = defussQuery();const iframeStates = ['default', 'loaded'];// VERIFIED: (verify's API docs gate) the states below are exactly the declared ones, each// described, and every config field typed, described and named in the code./** setState() configs per state - the iframe's states take none. */export interface IframeStateConfigs { /** Not loaded yet - the frame shows its placeholder shimmer. */ default: {}; /** The framed page loaded (set by the frame's load event, or by setState). */ loaded: {};}/** What an iframe-message event carries. */export interface IframeMessageDetail { /** the message name the sender chose */ name: string; /** its payload, structured-cloned across the frame boundary */ detail: unknown; /** the origin it came from ('null' for a sandboxed srcdoc frame) */ origin: string;}/** What an iframe-resize event carries. */export interface IframeResizeDetail { /** the framed page's content height in CSS pixels */ height: number;}/** The wire format both sides post - one shape, so a page without defuss-shadcn can speak it. */interface IframeWire { /** always 'defuss:iframe' - anything else on the channel is not ours */ type: 'defuss:iframe'; /** 'size' reports a content height, 'message' carries a named message, 'hello' (host to frame) asks for the height */ kind: 'size' | 'message' | 'hello'; /** the content height (kind 'size') */ height?: number; /** the message name (kind 'message') */ name?: string; /** the message payload (kind 'message') */ detail?: unknown;}const PROTOCOL = 'defuss:iframe';/** true inside a frame: this window has a parent of its own */const framed = (): boolean => (globalThis.parent as unknown) !== (globalThis as unknown);/** The markup of a state: 'loaded' marks the figure, 'default' clears the mark. */function applyMarkup(el, stateName) { dfDollar(el).attr('data-loaded', stateName === 'loaded' ? '' : null);}/** UI side of setState: the only function that writes a state onto the figure. */function triggerStateChange(el, stateName) { applyMarkup(el, stateName);}/** Registry-level API; pass the figure explicitly. Unknown names throw. */export const iframeApi = componentState({ component: 'iframe', states: iframeStates, apply: (el, state) => triggerStateChange(el, state.name), // the frame's own load event marks the figure - read the mark back read: (el, state) => ({ name: el.hasAttribute('data-loaded') ? 'loaded' : 'default', config: state.config }), markup: (el, state) => applyMarkup(el, state.name),});df$.iframeApi = iframeApi;df$.iframeStates = iframeStates;const frameOf = (el: HTMLElement): HTMLIFrameElement | undefined => dfDollar(el).children<HTMLIFrameElement>('iframe.iframe-frame').get(0);/** The origins a figure accepts messages from: its own page's, plus data-origins. */function accepts(el: HTMLElement, origin: string): boolean { if (origin === globalThis.location.origin) return true; return (dfDollar(el).attr('data-origins') ?? '').split(/\s+/).includes(origin);}/** The origin to post into a frame with: the frame's own when readable, else the first allowed one, else '*' for an opaque (sandboxed) frame. */function targetOrigin(el: HTMLElement, frame: HTMLIFrameElement): string { const listed = (dfDollar(el).attr('data-origins') ?? '').split(/\s+/).filter(Boolean); try { const own = frame.contentWindow?.location.origin; if (own && own !== 'null') return own; } catch { // cross-origin: the location is not readable } const named = listed.find((o) => o !== 'null'); return named ?? '*';}/** A content height arrived (measured or reported): store it as --iframe-height. */function setHeight(el: HTMLElement, height: number): void { const h = Math.ceil(height); if (!(h > 0) || el._iframeHeight === h) return; el._iframeHeight = h; el.style.setProperty('--iframe-height', `${h}px`); // Fires when the framed page's content height changes (data-fit="content") - the new height. el.dispatchEvent(new CustomEvent<IframeResizeDetail>('iframe-resize', { bubbles: true, detail: { height: h } }));}/** Measure a same-origin frame's document and follow it; false when it is not readable. */function followContent(el: HTMLElement, frame: HTMLIFrameElement): boolean { let doc: Document | null = null; try { doc = frame.contentDocument; } catch { doc = null; } if (!doc?.documentElement) return false; el._iframeObserver?.disconnect(); // the root element's box is the content: body margins cannot collapse through it const measure = () => setHeight(el, doc.documentElement.getBoundingClientRect().height); const ro = new ResizeObserver(measure); ro.observe(doc.documentElement); if (doc.body) ro.observe(doc.body); el._iframeObserver = ro; measure(); return true;}/** Follow a content-fit frame: measure it when same-origin, else ask it for its height - a first report * sent before this script ran would be lost, so the host greets and the framed page answers. */function follow(el: HTMLElement, frame: HTMLIFrameElement): void { if (dfDollar(el).attr('data-fit') !== 'content' || followContent(el, frame)) return; frame.contentWindow?.postMessage({ type: PROTOCOL, kind: 'hello' } satisfies IframeWire, targetOrigin(el, frame));}function onLoad(el: HTMLElement, frame: HTMLIFrameElement): void { applyMarkup(el, 'loaded'); follow(el, frame);}function init() { dfDollar('.iframe:not([data-init])').toArray().forEach((el: HTMLElement) => { el.dataset.init = ''; // el.store + el.api (AGENTS.md "State through stores") bindComponent(el, iframeApi); const frame = frameOf(el); if (!frame) return; dfDollar(frame).on('load', () => onLoad(el, frame)); follow(el, frame); // a page that finished before this script ran fired its load event unheard; the // document's own load event waits for every frame in it, so a frame present while // the document still loads is loaded when that event fires (a frame added later // fires its own load event, heard above) if (document.readyState !== 'complete') globalThis.addEventListener('load', () => onLoad(el, frame), { once: true }); });}// one listener for every framed page's messages: from a frame we host, from an allowed originif (!document.__iframeInit) { document.__iframeInit = true; globalThis.addEventListener('message', (e: MessageEvent) => { const data = e.data as IframeWire; if (!data || data.type !== PROTOCOL) return; // the parent page's messages to this (framed) page if (e.source === globalThis.parent && framed()) { const parentOrigin = dfDollar(document.documentElement).attr('data-iframe-parent'); if (parentOrigin && parentOrigin !== e.origin) return; // the host asks for the height (it may have missed the first report) if (data.kind === 'hello') { if (document.documentElement.hasAttribute('data-iframe-child')) sendToParent({ type: PROTOCOL, kind: 'size', height: Math.ceil(document.documentElement.getBoundingClientRect().height) }); return; } if (data.kind !== 'message') return; // Fires on the framed page's document when its host page posts a message (df$.shadcn.iframe.post) - the name, the payload and the host's origin. document.dispatchEvent(new CustomEvent<IframeMessageDetail>('iframe-message', { detail: { name: String(data.name ?? ''), detail: data.detail, origin: e.origin } })); return; } const el = dfDollar('.iframe[data-init]').toArray().find((f: HTMLElement) => frameOf(f)?.contentWindow === e.source) as HTMLElement | undefined; if (!el || !accepts(el, e.origin)) return; if (data.kind === 'size' && dfDollar(el).attr('data-fit') === 'content' && typeof data.height === 'number') setHeight(el, data.height); if (data.kind === 'message') { // Fires on the .iframe element when its framed page posts a message (df$.shadcn.iframe.send) - the name, the payload and the origin it came from. el.dispatchEvent(new CustomEvent<IframeMessageDetail>('iframe-message', { bubbles: true, detail: { name: String(data.name ?? ''), detail: data.detail, origin: e.origin } })); } });}/** Post one message to the host page - from a page that runs inside a frame. */function sendToParent(wire: IframeWire): boolean { if (!framed()) return false; const origin = dfDollar(document.documentElement).attr('data-iframe-parent') || '*'; globalThis.parent.postMessage(wire, origin); return true;}// a framed page that opts in (<html data-iframe-child>) reports its height while it changesif (framed() && document.documentElement.hasAttribute('data-iframe-child') && !document.__iframeChildInit) { document.__iframeChildInit = true; let last = 0; new ResizeObserver(() => { const height = Math.ceil(document.documentElement.getBoundingClientRect().height); if (height !== last) { last = height; sendToParent({ type: PROTOCOL, kind: 'size', height }); } }).observe(document.documentElement);}const resolve = (target: string | HTMLElement): HTMLElement | undefined => (typeof target === 'string' ? dfDollar(target).get(0) : target);df$.iframe = { /** * Post a named message into a framed page; it arrives there as an iframe-message event on the document (or as a plain message event carrying { type: 'defuss:iframe', kind: 'message', name, detail }). * @param target - the .iframe element or its selector * @param name - the message name * @param detail - the payload, structured-cloned * @returns false when the frame has no window yet */ post: (target: string | HTMLElement, name: string, detail?: unknown): boolean => { const el = resolve(target); const frame = el && frameOf(el); if (!el || !frame?.contentWindow) return false; frame.contentWindow.postMessage({ type: PROTOCOL, kind: 'message', name, detail } satisfies IframeWire, targetOrigin(el, frame)); return true; }, /** * Post a named message to the host page - called from inside a framed page; the host's .iframe element fires iframe-message. Set data-iframe-parent on the framed page's html element to the host origin to post to it only. * @param name - the message name * @param detail - the payload, structured-cloned * @returns false when this page is not framed */ send: (name: string, detail?: unknown): boolean => sendToParent({ type: PROTOCOL, kind: 'message', name, detail }), /** * Measure a same-origin frame's content again (data-fit="content"). * @param target - the .iframe element or its selector * @returns false when the framed document is not readable (cross-origin: it reports its own height) */ resize: (target: string | HTMLElement): boolean => { const el = resolve(target); const frame = el && frameOf(el); return !!el && !!frame && followContent(el, frame); },};init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub