HtmlPreview
Interactive HTML/CSS preview with viewport switching and collapsible source code display.
<HtmlPreview> renders live HTML/CSS demos inside an isolated iframe with viewport presets (Mobile / Tablet / Full) and a collapsible source code panel with syntax highlighting. It is globally available in all MDX files without imports.
Basic Usage
<HtmlPreview
title="Basic Box"
html={`
<div class="box">Hello, world!</div>
`}
css={`
.box {
padding: 24px;
background: #3b82f6;
color: #fff;
border-radius: 8px;
font-family: system-ui, sans-serif;
text-align: center;
}
`}
/>Responsive Layout
Use the viewport buttons (Mobile / Tablet / Full) to see how content reflows at different widths. Try resizing with the drag handle at the bottom-right of the preview area.
HTML Only
When no css prop is provided, only the HTML source is shown.
Default Open Code
Use defaultOpen to show the source code expanded by default.
<button class="btn">Click me</button>.btn {
padding: 10px 20px;
background: #8b5cf6;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-family: system-ui, sans-serif;
cursor: pointer;
}
.btn:hover {
background: #7c3aed;
}Fixed Height
Use the height prop to set a fixed iframe height instead of auto-sizing.
Full Height
Use the fullHeight prop to make the preview document's html/body stretch to fill the iframe — useful for layouts that rely on height: 100% reaching the viewport (e.g. a flex column that fills the available space).
Pair with an explicit height
fullHeight interacts with the auto-height mechanism: auto-height measures the iframe body and resizes the iframe to fit, but fullHeight makes the body's height derive from the iframe instead — combining the two creates a feedback loop with no stable resolution. Always set an explicit height alongside fullHeight.
External Resources
You can inject external resources (CSS frameworks, webfonts, scripts) into previews. Configure globally via zfb.config.ts or per-component via props.
Global Configuration
Set htmlPreview in zfb.config.ts to apply resources to all previews:
export default defineConfig(
zudoDoc({
htmlPreview: {
head: `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap">`,
css: `body { font-family: 'Noto Sans JP', sans-serif; }`,
js: `console.log('preview loaded');`,
},
}),
);Per-Component Props
Use head and js props to add resources to individual previews. These are merged after global values.
<HtmlPreview
title="With JS"
html={`
<div id="output">Waiting...</div>
`}
css={`
#output {
padding: 16px;
font-family: system-ui, sans-serif;
color: #334155;
}
`}
js={`
document.getElementById('output').textContent = 'Hello from JS!';
`}
/>External Stylesheets & Scripts
head and js are stringly-typed and always render into a visible "Head"/"JS" code block, and the preflight reset (Tailwind v4 CSS reset injected into every preview) is always applied on top — no way to skip it for a framework that ships its own. externalStyles, externalScripts, preflight, and showResources give CDN resources (a CSS framework, webfont, or script like @tailwindcss/browser, Bootstrap, or htmx) a structured, code-panel-aware alternative:
externalStyles— array of stylesheet URLs, emitted as<link rel="stylesheet" href="...">tags. Loaded before the authorcss, socsscan still override the framework.externalScripts— array of script URLs, emitted as<script src="...">tags. Flows through the same sandbox/syncDelayderivation as an inlinejsprop — the preview automatically getssandbox="allow-scripts allow-same-origin"and the 300ms height re-sync delay.preflight— set tofalseto skip the injected preflight reset entirely, for a framework (like Tailwind) that ships its own base styles.showResources— both arrays are excluded from the visible code panel by default (unlikehead/js); set totrueto surface them as literal<link>/<script src>lines at the top of the "HTML" panel when it's pedagogically useful to show the reader what's loaded.
These four props are per-usage only — unlike head/css/js, they are not part of the global htmlPreview configuration in zfb.config.ts.
External resources load client-side, not at build time
externalStyles/externalScripts are network requests the browser makes when the preview iframe renders, not assets bundled at build time. The preview may briefly show unstyled/unstyled content while the resource loads, and it depends on the resource staying available at that URL.
A one-line Tailwind CDN demo, using preflight={false} since @tailwindcss/browser ships its own reset:
<HtmlPreview
title="Tailwind CDN"
externalScripts={["https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"]}
preflight={false}
html={`
<div class="flex gap-4 p-6 bg-slate-100">
<div class="px-4 py-2 bg-blue-500 text-white rounded-lg font-sans">Tailwind</div>
<div class="px-4 py-2 bg-emerald-500 text-white rounded-lg font-sans">via CDN</div>
</div>
`}
/>With showResources, the CDN URL is shown as a literal line at the top of the "HTML" code panel instead of being hidden:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<div class="px-4 py-2 bg-violet-500 text-white rounded-lg font-sans w-fit">Show code to see the CDN line</div>Security & the sandbox prop
Previews render inside an isolated <iframe srcdoc>. Its sandbox attribute defaults to allow-scripts allow-same-origin when the preview contains scripts (a js prop, a <script> in head, or a non-empty externalScripts), and allow-same-origin otherwise.
Trust assumption
allow-scripts + allow-same-origin together void the iframe sandbox — scripts inside the preview share the parent page's origin and can reach the parent document. zudo-doc keeps this default because preview content is author-trusted MDX, and allow-same-origin is what powers the auto-height measurement.
If your project renders semi-trusted or user-submitted HTML, override it with a stricter value via the sandbox prop.
<!-- Maximally restrictive: no script execution, opaque origin -->
<HtmlPreview html={untrusted} sandbox="" height={400} />
<!-- Allow scripts but keep an opaque origin (script can't reach the parent) -->
<HtmlPreview html={untrusted} sandbox="allow-scripts" height={400} />Removing allow-same-origin gives the iframe an opaque origin, which blocks the parent from reading iframe.contentDocument — so it disables auto-height. Always pair a stricter sandbox with a fixed height. The empty string "" is honored verbatim; only omitting the prop falls back to the computed default.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
html | string | (required) | HTML content to render inside the preview iframe |
css | string | undefined | CSS styles applied inside the preview iframe |
head | string | undefined | Raw HTML injected into <head> (links, meta, fonts) |
js | string | undefined | JavaScript executed inside the preview iframe |
title | string | undefined | Title displayed in the preview header bar |
height | number | auto | Fixed iframe height in pixels. When omitted, height auto-adjusts to content |
defaultOpen | boolean | false | Show the source code panel expanded by default |
fullHeight | boolean | false | Makes the preview document's html/body stretch to 100% height. Interacts with auto-height — always pair with an explicit height |
sandbox | string | auto | iframe sandbox attribute. Omit for the computed default (allow-scripts allow-same-origin with scripts, allow-same-origin without). Pass a stricter value for untrusted content — but disables auto-height when allow-same-origin is dropped, so set height too |
externalStyles | string[] | undefined | External stylesheet URLs, injected as <link rel="stylesheet"> before head/css. Per-usage only — loads client-side at view time, not build-bundled |
externalScripts | string[] | undefined | External script URLs, injected as <script src>. Flips the sandbox/syncDelay derivation the same as js. Per-usage only — loads client-side at view time, not build-bundled |
preflight | boolean | true | Set to false to skip the injected preflight reset — for a framework (loaded via externalStyles/externalScripts) that ships its own |
showResources | boolean | false | Surfaces externalStyles/externalScripts as literal lines at the top of the "HTML" code panel. Excluded from the panel by default |
Syntax highlighting and lazy WASM
The source code panel lazily imports the public @takazudo/zfb-md-wasm package root and calls its semantic highlighter for:
html— for the HTML and Head panelscss— for the CSS paneljavascript— for the JS panel
The renderer emits safely escaped pre.hi-root / hi-* markup and shares the document-fence --zd-syntax-* palette. Opening the panel is the lazy boundary: the emitted JavaScript glue and WASM companion resources are not requested beforehand. A warning for an unknown language uses escaped semantic fallback markup; import, initialization, invalid-option, or current-call failures keep the JSX-escaped plain <pre><code> fallback. A later panel render can retry a transient import failure.
If you serve production output through custom infrastructure, preserve the generated .mjs and .wasm assets and serve them with JavaScript and application/wasm MIME types. Do not copy package-internal glue paths manually; the zfb build owns the resource graph.
This browser-time set is smaller than the full language list available to standard build-time code blocks.
Notes
The preview renders inside an isolated
<iframe>with a CSS reset (Tailwind v4 preflight), so styles do not leak in or out.Previews default to
sandbox="allow-same-origin"(orsandbox="allow-scripts allow-same-origin"when ajsprop or<script>is present) so the iframe height auto-syncs viacontentDocument. Thesrcdoccontent is author-controlled MDX. Override with thesandboxprop for untrusted content — see Security & thesandboxprop above.Global resources from the
htmlPreviewconfig field are injected before per-component props.The
html,css, andjsprops support template literals with indentation. Leading whitespace is automatically stripped (dedented) in the source code display.Client-side hydration is handled automatically by the component wrapper — no
client:loaddirective needed in MDX.externalStyles/externalScriptsload client-side at view time — they are network requests the browser makes when the preview iframe renders, not assets bundled at build time. See External Stylesheets & Scripts above.Injection order in the srcdoc: preflight reset (unless
preflight={false}) →fullHeightstyle →externalStyles→head→css.