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.
<Frame>
Section titled “<Frame>”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.
useIframe: the headless version
Section titled “useIframe: the headless version”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.
Why not just portal into contentDocument?
Section titled “Why not just portal into contentDocument?”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.
Styles
Section titled “Styles”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 atuseFrame().document.headinstead, as above; adoptedStyleSheetsare copied once, when mirroring starts.