Theme
Design your own
On this page (9)
Component Skill — components/iframe/component-skill.md

Native basis

<figure class="iframe"> around a native <iframe class="iframe-frame"> and an optional <figcaption class="iframe-caption">.

Web Platform APIs

<iframe>postMessage()ResizeObserveraspect-ratioX-Frame-Options

Classes

.iframe.iframe-frame.iframe-caption

Data attributes

• data-fit - (none): full width, height from --iframe-ratio; container: fill the element it lives in; content: follow the framed page's height

• data-origins - space-separated origins whose messages are accepted besides the page's own (null for a sandboxed srcdoc)

• data-variant="bare" - no border, radius or placeholder

• on the framed page's <html>: data-iframe-child reports its height, data-iframe-parent names the only origin it posts to

Notes

• The embedded site decides whether it can be framed: X-Frame-Options or CSP frame-ancestors make the browser refuse it, and no script can tell - caption such embeds with a link. Messages are accepted only from a frame this element hosts and from an allowed origin.

§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:

<!-- 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-loaded on 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:

StateTypeValuesDefaultDescription
loadedbooleantrue, falsefalseThe 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.

StateDescription
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

MemberDescription
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.
ArgumentTypeDescription
nameSa declared state (an unknown name throws)
config?IframeStateConfigs[S]that state's config (merged into the stored one when the component merges)

Returns unknown - what the state's DOM work returned - a Promise for an async state (or await settled())

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 { name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel } - the state's name, its config and the authored markup model render() starts from

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.
ArgumentTypeDescription
state?{ name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel }a state as getState() returns it (default: the current one)

Returns string - the element's outer HTML in that state

el.api.settled(): Promise<void>
Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting).

Returns Promise<void> - resolves when nothing is pending

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

MemberDescription
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.
ArgumentTypeDescription
elHTMLElementthe component's element
nameSa declared state (an unknown name throws)
config?IframeStateConfigs[S]that state's config (merged into the stored one when the component merges)

Returns unknown - what the state's DOM work returned - a Promise for an async state (await it, or el.api.settled())

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.
ArgumentTypeDescription
elHTMLElementthe component's element

Returns { name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel } - the state's name, its config and the authored markup model render() starts from

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.
ArgumentTypeDescription
state{ name: IframeState; config: IframeStateConfigs[IframeState]; model?: ElementModel }a state as getState() returns it (with its model)

Returns string - the element's outer HTML in that state

df$.shadcn.iframeApi.store(el: HTMLElement): Store<{ name: IframeState; config: IframeStateConfigs[IframeState] }>
The element's store (bindComponent made it).
ArgumentTypeDescription
elHTMLElementthe component's element

Returns Store<{ name: IframeState; config: IframeStateConfigs[IframeState] }> - a defuss-store store of { name, config } - subscribe to follow every change, set it to drive the component

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.
ArgumentTypeDescription
elHTMLElementthe component's element
nameSthe state it is in
config?IframeStateConfigs[S]its config
df$.shadcn.iframeStates: IframeState[]The declared states, 'default' first: default, loaded.

df$.shadcn.iframe

MemberDescription
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 }).
ArgumentTypeDescription
targetstring | HTMLElementthe .iframe element or its selector
namestringthe message name
detail?unknownthe payload, structured-cloned

Returns boolean - false when the frame has no window yet

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.
ArgumentTypeDescription
namestringthe message name
detail?unknownthe payload, structured-cloned

Returns boolean - false when this page is not framed

resize(target: string | HTMLElement): boolean
Measure a same-origin frame's content again (data-fit="content").
ArgumentTypeDescription
targetstring | HTMLElementthe .iframe element or its selector

Returns boolean - false when the framed document is not readable (cross-origin: it reports its own height)

Events

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

detail: IframeMessageDetail

FieldTypeDescription
namestringthe message name the sender chose
detailunknownits payload, structured-cloned across the frame boundary
originstringthe origin it came from ('null' for a sandboxed srcdoc frame)
iframe-resize
Fires when the framed page's content height changes (data-fit="content") - the new height.

detail: IframeResizeDetail

FieldTypeDescription
heightnumberthe framed page's content height in CSS pixels

Types

TypeDescription
IframeMessageDetail
What an iframe-message event carries.
FieldTypeDescription
namestringthe message name the sender chose
detailunknownits payload, structured-cloned across the frame boundary
originstringthe origin it came from ('null' for a sandboxed srcdoc frame)
IframeResizeDetail
What an iframe-resize event carries.
FieldTypeDescription
heightnumberthe framed page's content height in CSS pixels

§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 origin
if (!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 changes
if (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