Skip to content

Portal rendering

Rendering into an iframe isolates the content’s CSS and layout from the host page: emails, invoices, print previews, embedded editors, design-system sandboxes.

import { Frame } from 'react-iframe-kit';
<Frame title="Email preview" head={<style>{emailCss}</style>} resize>
<Email message={message} />
</Frame>;
Prop
children Rendered into the iframe’s <body>.
head Rendered into the iframe’s <head>, e.g. <style> or <link>.
copyStyles Mirror the parent’s <style> and <link rel="stylesheet"> into the iframe, including ones added later.
resize true to follow the content’s height, or useIframeResize options.
srcDoc A custom document, as a string or a TrustedHTML (Trusted Types). Keep it stable: changing it reloads the iframe.
anything else Forwarded to the <iframe>. ref points at the iframe element.

Screen readers announce an iframe by its title, so give every <Frame> one that describes its content. In development, <Frame> warns about a missing or generic title ("iframe", a URL), a title another mounted <Frame> already has, and a negative tabIndex, which keeps keyboard users out of the content.

Inside a <Frame>, useFrame() returns the iframe’s window and document. CSS-in-JS and positioning libraries need those:

import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { useFrame } from 'react-iframe-kit';
function FrameEmotion({ children }: { children: React.ReactNode }) {
const { document } = useFrame();
const cache = useMemo(
() => document && createCache({ key: 'frame', container: document.head }),
[document],
);
return cache ? <CacheProvider value={cache}>{children}</CacheProvider> : null;
}

Outside a <Frame>, useFrame() returns the global window and document, so the same component works in both places. On the server both are null.

import { createPortal } from 'react-dom';
import { useIframe } from 'react-iframe-kit';
function Preview({ children }: { children: React.ReactNode }) {
const { frameProps, mountNode, error } = useIframe();
if (error) return <p>Can't render the preview.</p>;
return (
<>
<iframe {...frameProps} title="Preview" />
{mountNode && createPortal(children, mountNode)}
</>
);
}

mountNode, window and document are null until the iframe’s final document has loaded, and they’re updated after every reload.

When an <iframe> is inserted, the browser creates an initial about:blank document right away. If you portal into it and that document is then replaced, the content can disappear without an error: React keeps rendering into a detached document. With a srcdoc, the initial document is replaced in every browser.

react-iframe-kit gives the iframe a srcdoc of its own, which also keeps the document in standards mode (about:blank is quirks mode). It mounts only after that document’s native load, and remounts after every reload.

copyStyles clones the parent’s stylesheets at mount and mirrors later changes (dev HMR, runtime CSS-in-JS). Its limits:

  • only direct children of the parent <head> are copied;
  • rules added through the CSSOM (sheet.insertRule, e.g. emotion’s production “speedy” mode) can’t be observed, so they aren’t mirrored: point the library at useFrame().document.head instead, as above;
  • adoptedStyleSheets are copied once, when mirroring starts.