HtmlPreview
ビューポート切り替えとソースコード表示を備えたインタラクティブなHTML/CSSプレビューコンポーネント。
<HtmlPreview> は、分離された iframe 内で HTML/CSS のライブデモを描画するコンポーネントです。ビューポートプリセット(Mobile / Tablet / Full)とシンタックスハイライト付きの折りたたみ可能なソースコードパネルを備えています。すべての MDX ファイルでインポートなしで使用できます。ルートにバインドされた MDX のバインディングはプレビューのクロームをローカライズしますが、ルートにバインドされていないコンポーネントを直接使う場合はロケール情報がなく、labelsを渡さない限り英語ラベルが使われます。
ローカライズされたコントロール
ルートにバインドされた MDX のバインディングでコンポーネントを描画すると、アクティブなページのロケールから次の7つのプレビュー用クローム文字列が提供されます。
| 翻訳キー | UIでの用途 |
|---|---|
htmlPreview.viewport.mobile | Mobileビューポートボタン |
htmlPreview.viewport.tablet | Tabletビューポートボタン |
htmlPreview.viewport.full | Full幅ビューポートボタン |
htmlPreview.viewport.label | ビューポートプリセットグループのaria-label |
htmlPreview.source.show | 折りたたまれたソース切り替えのラベル |
htmlPreview.source.hide | 展開されたソース切り替えのラベル |
htmlPreview.iframe.title | 表示用のtitleが省略されたときの外側のiframeタイトル。また、作成者の<title>も空白でないtitleも指定されていないときの生成ドキュメントタイトルのフォールバック |
値の検索順は、要求されたロケール → 設定済みのデフォルトロケール → パッケージの英語テーブル → 生のキーです。
作成者が渡すtitleプロップはタイトルバーを制御します。空白でない値は、マージ済みのheadに作成者の<title>がない場合、生成される iframe ドキュメントタイトルにも使われます。作成者のタイトルがある場合は、そちらが優先されます。技術的なコードパネル見出し(HTML、CSS、Head、JS)は意図的に固定されており、これらの翻訳キーではローカライズされません。
1つのプレビューだけをカスタマイズするには、部分的なlabelsオブジェクトを渡します。省略したキー(undefinedを設定したキーを含む)は、ルートのロケール値を消去せず、その値を維持します。
<HtmlPreview
html="<p>One preview</p>"
labels={{
mobile: "Phone",
preview: "Live preview",
}}
/>ルートにバインドされていないコンポーネントを直接インポートする場合、labelsを省略したキーには組み込みの英語デフォルトが使われます。その利用箇所を別の言語にするには、同じ部分オブジェクト(または7つすべてのキー)を渡してください。
プレビュードキュメントのメタデータ
生成されるすべてのプレビュードキュメントには、空でない<html lang>の値が設定されます。信頼されたheadが開き<title>を管理していない場合は、空でないドキュメント<title>も追加されます。langの値は、コンポーネントの利用方法に応じて次のように選択されます。
| 利用方法 | 言語の優先順位 |
|---|---|
ルートにバインドされた MDX <HtmlPreview> ラッパー | 空白でない明示的なlang → アクティブなルートロケール → en |
低レベルのHtmlPreviewを直接インポート | 空白でない明示的なlang → en |
空の、または空白文字だけのlangは省略されたものとして扱われます。空白でない言語タグは任意の値を受け付け、属性コンテキストに応じてエスケープしたうえでシリアライズされます。この値は生成されるプレビュードキュメントの<html lang>のメタデータであり、外側のページの言語を変更するものではありません。
生成されるドキュメントタイトルの優先順位は次のとおりです。
マージ済みの
headにある信頼された作成者の開き<title>は、作成者が管理する権威ある値としてそのまま残り、生成タイトルは追加されません。空白でない
titleプロップが生成ドキュメントタイトルを指定します。マージ済みのローカライズ済み/呼び出し単位の
labels.previewが生成ドキュメントタイトルを指定します。最終フォールバックとしてリテラル値
Previewが使われます。
空の、または空白文字だけのtitleとlabels.previewは、次の候補へフォールスルーします。公開 API には独立したdocumentTitleプロップは意図的にありません。作成者がドキュメントの<title>を管理する必要がある場合はheadを使ってください。コンポーネントが生成するメタデータだけがコンテキストに応じてエスケープされます(langは HTML 属性として、生成タイトルは HTML テキストとして扱われます)。自由形式のheadは信頼された呼び出し元のコンテンツとして、指定どおりのバイト列で保持されます。
完全な eager プレビューと、loading="visible"の可視性ゲートが開いた後にマウントされるプレビューでは、同じメタデータの優先順位とエスケープが適用されます。visible モードのサーバー予約領域には、マウントされるまでプレビュードキュメントは含まれません。
<HtmlPreview
lang="fr-CA"
title="Visible title"
labels={{ preview: "Localized fallback" }}
head={'<title>Author title</title>'}
html="<p>Language and title metadata are serialized.</p>"
/>遅延読み込み
ルートにバインドされた MDX の <HtmlPreview> ラッパーは、loading="eager" または loading="visible" を受け付けます。デフォルトは "eager" で、省略した場合は "eager" を渡した場合と同じです。これはラッパーのライフサイクルポリシーであり、iframe のネイティブな loading 属性ではありません。また、iframe に転送されません。低レベルの HtmlPreview を直接インポートした場合、このプロップは使えません。
デフォルトの "eager" モードでは、iframe、srcdoc、任意のクロームを含むプレビューのサブツリー全体をサーバーが描画します。クライアント側のハイドレーションには、引き続き zfb の通常の可視時アイランドタイミングが使われます。
"visible" では、サーバー出力は不活性で aria-hidden な高さ予約(明示した正の height、または 200px のフォールバック)だけになります。プレビューの iframe や srcdoc は含まれず、プレビューのコントロール、インラインスクリプト、外部リソース用タグも、プレビューがマウントされる前に実行・読み込みされることはありません。予約領域がビューポートと交差すると、ラッパーは完全なプレビューを一度だけマウントし、通常の動作を開始します。
zfb 2.14.2 は、可視マーカーの値に関係なく skip-SSR の render ターゲットを直ちにマウントします。この遅延を維持するため、ラッパーの内部ターゲットは予約領域に対して一度だけの IntersectionObserver ゲートを適用し、最初に交差したエントリの後で切断します。IntersectionObserver が利用できない場合はフェイルオープンし、プレビューを直ちにマウントします。
どちらのモードでも、プレビューが描画された後は既存のルートローカライズ済みラベルと任意の showSource / showViewportControls クロームが維持されます。
基本的な使い方
<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;
}
`}
/>レスポンシブレイアウト
ビューポートボタン(Mobile / Tablet / Full)を使って、異なる幅でコンテンツがどのように変化するかを確認できます。プレビュー領域の右下にあるドラッグハンドルでリサイズすることもできます。
HTML のみ
css、head、js のいずれのプロップも指定しない場合は、HTML のソースのみが表示されます。
デフォルトでコードを表示
defaultOpen を指定すると、ソースコードが最初から展開された状態で表示されます。
<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;
}任意のコントロール領域
2つの任意のコントロール領域はデフォルトで表示されます。showSource={false}を設定すると、ソース切り替え、コードパネル、ハイライト用マークアップが構造的に省略されます。プレビューの iframe は残ります。defaultOpenはコードパネルの状態を初期化するだけなので、ソース領域を隠した場合は効果がありません。
showViewportControls={false}を設定すると、Mobile / Tablet / Full のプリセットグループとボタンが構造的に省略されます。プレビューはFull幅(100%)のコンテナと水平方向のドラッグリサイズ操作を保持します。プリセットを隠してもリサイズは無効になりません。
2つのフラグは同時にfalseにできます。iframeは引き続き描画されます。タイトルバーはビューポートコントロールが表示されているか、空でないtitleが指定されている場合に描画されます。したがって、プリセットを隠してもタイトルを指定すればタイトルだけのバーが残り、プリセットとtitleの両方を省略すれば空のバーは削除されます。
固定高さ
height プロップを使って、自動調整の代わりに iframe の高さを固定できます。iframe は指定したピクセル値を正確な高さとして維持し、高さ自動調整コントローラーは ResizeObserver を設定せず、コンテンツの増減でも高さを変更しません。
不透明なサンドボックスでは固定高さが必須です。allow-same-origin を外すと親から iframe ドキュメントを読み取れなくなります。不透明オリジン向けの postMessage によるリサイズ経路はないため、より厳しい sandbox には height を併用してください。
高さ自動調整のライフサイクル
height を省略し、fullHeight が false の場合、読み取り可能な同一オリジンのプレビューは iframe の読み込み後にドキュメントを計測し、iframe の高さを同期し続けます。デフォルトのサンドボックスには allow-same-origin が含まれているため、親からこの目的でプレビュードキュメントを読み取れます。
プレビューは iframe ドキュメントの body と html を ResizeObserver で監視します。そのため、後から発生するコンテンツの伸縮に加えて、遅れて読み込まれるスタイルシート、フォント、画像、スクリプトによるジオメトリの変化も反映されます。ビューポートプリセットを変更した場合も、コンテンツのリフロー後に再計測がスケジュールされます。スクリプトを含むプレビューでは、読み込み直後の計測に加えて、読み込み後300msに1回の追従計測も行われます。
iframe が ResizeObserver を提供しない場合も、直後の計測と、スクリプトを含む場合の遅延した一度だけの計測は維持されますが、継続的な追従はできません。不透明なサンドボックスは計測できないため、代わりに固定の height を使ってください。
高さ100%
fullHeight プロップを使うと、プレビュードキュメントの html/body を iframe いっぱいに広げられます。height: 100% がビューポートまで届くレイアウト(利用可能なスペースを埋めるフレックスカラムなど)に便利です。
明示的な height と併用する
fullHeight は高さ自動調整の仕組みと干渉します — 高さ自動調整は iframe の body を計測して iframe の高さをそれに合わせますが、fullHeight は逆に body の高さを iframe 側から導出させます。両方を組み合わせると、安定した解決策のないフィードバックループが発生します。固定の height がない場合、fullHeight は意図的に高さ自動調整を無効にし、デフォルトの200px高さで ResizeObserver なしになります。正しい使い方は fullHeight と明示的な height を併用することです。
外部リソース
CSS フレームワーク、Web フォント、スクリプトなどの外部リソースをプレビューに読み込めます。zfb.config.ts でグローバルに設定するか、コンポーネントのプロップで個別に指定します。
グローバル設定
zfb.config.ts で htmlPreview を設定すると、すべてのプレビューにリソースが適用されます:
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');`,
},
}),
);コンポーネント単位のプロップ
head と js プロップを使って、個別のプレビューにリソースを追加できます。グローバル設定の後にマージされます。
<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!';
`}
/>外部スタイルシートとスクリプト
head と js は文字列ベースのプロップで、常に可視の「Head」/「JS」コードブロックとして表示され、プレビューには preflight リセット(すべてのプレビューに注入される Tailwind v4 の CSS リセット)が常に上乗せで適用され、独自リセットを持つフレームワークでもスキップする手段がありません。externalStyles・externalScripts・preflight・showResources は、CDN リソース(CSS フレームワーク、Web フォント、@tailwindcss/browser・Bootstrap・htmx のようなスクリプト)向けに、コードパネルを意識した構造化された代替手段を提供します。
externalStyles— スタイルシート URL の配列。<link rel="stylesheet" href="...">タグとして出力されます。author のcssより前に読み込まれるため、cssでフレームワークを上書きできます。externalScripts— スクリプト URL の配列。<script src="...">タグとして出力されます。インラインのjsプロップと同じサンドボックス/syncDelayの導出ロジックを通るため、プレビューは自動的にsandbox="allow-scripts allow-same-origin"と 300ms の高さ再同期ディレイを得ます。preflight—falseを指定すると、注入される preflight リセットを完全にスキップします。Tailwind のように独自のベーススタイルを持つフレームワーク向けです。showResources— 両方の配列は(head/jsと異なり)デフォルトでは可視のコードパネルから除外されます。読者に読み込まれているリソースを示したい場合はtrueを指定すると、「HTML」パネルの先頭に<link>/<script src>の生の行として表示されます。
これら4つのプロップはコンポーネント単位のみです — head/css/js と異なり、zfb.config.ts のグローバル htmlPreview 設定には含まれません。
外部リソースはクライアントサイドで読み込まれ、ビルド時には読み込まれない
externalStyles/externalScripts は、プレビューの iframe が描画されるときにブラウザが行うネットワークリクエストであり、ビルド時にバンドルされるアセットではありません。リソースの読み込み中はプレビューが一瞬スタイル未適用または一部適用の状態で表示されることがあり、そのURLでリソースが利用可能であり続けることに依存します。
loading="visible" では可視性ゲートが開くまで iframe が描画されないため、外部リソースへのリクエストとインラインの head / js の処理は、プレビューがマウントされるまで待機します。loading="eager" では完全なサーバー描画プレビューの動作が維持されます。
@tailwindcss/browser は独自のリセットを持つため preflight={false} を指定する、Tailwind CDN の1行デモ:
<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>
`}
/>showResources を指定すると、CDN の URL が非表示ではなく「HTML」コードパネルの先頭に生の行として表示されます:
<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>セキュリティと sandbox プロップ
プレビューは分離された <iframe srcdoc> 内に描画されます。その sandbox 属性は、プレビューにスクリプトが含まれる場合(js プロップ、head 内の <script>、または空でない externalScripts)は allow-scripts allow-same-origin、含まれない場合は allow-same-origin をデフォルトとします。
信頼の前提
allow-scripts と allow-same-origin を組み合わせると、iframe のサンドボックスは実質的に無効化されます — プレビュー内のスクリプトが親ページのオリジンを共有し、親ドキュメントにアクセスできます。zudo-doc がこのデフォルトを採用しているのは、プレビューの内容が作成者が信頼できる MDX であるためで、また allow-same-origin が高さの自動計測を可能にしているためです。
半信頼またはユーザー投稿の HTML を描画するプロジェクトでは、sandbox プロップでより厳しい値に上書きしてください。
<!-- 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} />allow-same-origin を外すと iframe のオリジンが不透明になり、親から iframe.contentDocument を読み取れなくなります — つまり高さの自動調整が無効になります。厳しい sandbox を指定するときは、必ず固定の height も併用してください。空文字列 "" はそのまま尊重され、プロップを省略したときだけ計算済みのデフォルトにフォールバックします。
Props
次の表は、ルートにバインドされた MDX の <HtmlPreview> ラッパーについて説明しています。loading はラッパー専用です。低レベルの HtmlPreview を直接インポートした場合、以下のプレビュー用プロップは使えますが、loading は使えません。
| プロップ | 型 | デフォルト | 説明 |
|---|---|---|---|
html | string | (必須) | プレビュー iframe 内に描画する HTML コンテンツ |
loading | "eager" | "visible" | "eager" | ラッパーのライフサイクルポリシー。"visible" はビューポートとの交差までプレビューのサブツリーを遅延する。ネイティブ iframe の loading 属性としては転送されない |
css | string | undefined | プレビュー iframe 内に適用する CSS スタイル |
head | string | undefined | <head> に挿入する信頼された生の HTML(リンク、メタ、フォント)。作成者の開き<title>は呼び出し元が管理し、生成メタデータより優先される |
js | string | undefined | プレビュー iframe 内で実行する JavaScript |
title | string | undefined | プレビューヘッダーバーに表示するタイトル。作成者の<title>がない場合は生成される iframe ドキュメントタイトルにも使われる |
lang | string | ルートロケール(直接利用ではen) | 生成されるプレビュードキュメントの<html lang>に使う言語タグ。バインドされた利用では空白でない明示的なlang → アクティブなルートロケール → en、直接利用では空白でない明示的なlang → enの順で解決され、空白値はフォールスルーする |
height | number | auto | iframe の固定高さ(ピクセル)。省略時はコンテンツに合わせて自動調整 |
defaultOpen | boolean | false | ソースコードパネルをデフォルトで展開表示する |
labels | Partial<HtmlPreviewLabels> | undefined | ビューポート、ソース、iframeコントロールの呼び出し単位のラベル。未指定キーはルートロケールのラベル(直接利用では英語デフォルト)を維持し、優先度の高いタイトル候補がない場合はpreviewが生成される iframe ドキュメントタイトルにも使われる |
showSource | boolean | true | ソース切り替えとコードパネルを描画する。falseでは構造的に削除するがiframeは残り、その場合defaultOpenは効果がない |
showViewportControls | boolean | true | Mobile / Tablet / Fullのプリセットコントロールを描画する。falseでもFull幅とドラッグリサイズ操作は残る |
fullHeight | boolean | false | プレビュードキュメントの html/body を高さ100%まで広げる。高さ自動調整と干渉するため、必ず明示的な height と併用する |
sandbox | string | auto | iframe の sandbox 属性。省略時は計算済みのデフォルト(スクリプトありなら allow-scripts allow-same-origin、なしなら allow-same-origin)。信頼できないコンテンツにはより厳しい値を指定 — ただし allow-same-origin を外すと高さ自動調整が無効になるため height も併せて指定する |
externalStyles | string[] | undefined | 外部スタイルシート URL。head/css より前に <link rel="stylesheet"> として挿入される。コンポーネント単位のみ — ビルドバンドルされず、表示時にクライアントサイドで読み込まれる |
externalScripts | string[] | undefined | 外部スクリプト URL。<script src> として挿入される。js と同様にサンドボックス/syncDelay の導出を切り替える。コンポーネント単位のみ — ビルドバンドルされず、表示時にクライアントサイドで読み込まれる |
preflight | boolean | true | false を指定すると、注入される preflight リセットをスキップする — externalStyles/externalScripts で読み込む独自リセットを持つフレームワーク向け |
showResources | boolean | false | externalStyles/externalScripts を「HTML」コードパネルの先頭に生の行として表示する。デフォルトではパネルから除外される |
シンタックスハイライトと遅延 WASM
ソースコードパネルは公開サブパス @takazudo/ を遅延インポートし、次の言語をセマンティックハイライターで処理します:
html— HTML パネルおよび Head パネルcss— CSS パネルjavascript— JS パネル
レンダラーは安全にエスケープされた pre.hi-root / hi-* マークアップを出力し、ドキュメントフェンスと同じ --zd-syntax-* パレットを共有します。遅延読み込みの境界はパネルを開く操作です。それより前に、生成された JavaScript glue と WASM companion リソースがリクエストされることはありません。未知の言語に対する warning は、エスケープ済みのセマンティックフォールバックマークアップを使用します。インポート、初期化、不正なオプション、現在の呼び出しの失敗時には、JSX でエスケープされたプレーンな <pre><code> フォールバックを維持します。一時的なインポート失敗は、後続のパネル描画で再試行できます。
コードパネルのHTML、CSS、Head、JS見出しは技術的なラベルであり、意図的に固定されています。htmlPreview.*翻訳キーは周囲のコントロールを対象とし、これらの見出しは対象外です。
独自の配信基盤で本番出力を提供する場合は、生成された .mjs / .wasm アセットを保持し、JavaScript と application/wasm の正しい MIME type で配信してください。パッケージ内部の glue パスを手作業でコピーしないでください。リソースグラフは zfb のビルドが管理します。
このブラウザ実行時の言語セットは、通常のビルド時コードブロックで利用できる言語一覧より限定されています。
注意事項
プレビューは CSS リセット(Tailwind v4 preflight)を含む分離された
<iframe>内で描画されるため、スタイルが外部に漏れることはありません。loading="visible"はラッパーの一度だけの可視性ゲートが開くまで不活性な高さ予約を保持します。ネイティブ iframe の遅延読み込み属性は追加しません。ルートにバインドされた MDX プレビューは7つの
htmlPreview.*コントロールラベルにアクティブなロケールを使います。ルートにバインドされていないコンポーネントを直接使う場合は、labelsを渡さない限り英語がデフォルトです。showSource={false}はiframeを削除せず、ソース切り替えとハイライト用サブツリーを構造的に削除します。隠されたソース領域をdefaultOpenで再び開くことはできません。showViewportControls={false}はプリセットボタンを削除しますが、Full幅のコンテナと水平方向のドラッグリサイズ操作は残ります。2つのフラグを同時にfalseにできます。空でないtitleを指定すればタイトルだけのバーが残り、空のバーは省略されます。プレビューはデフォルトで
sandbox="allow-same-origin"(jsプロップまたは<script>がある場合はsandbox="allow-scripts allow-same-origin")を使用し、contentDocument経由で iframe の高さを自動同期します。srcdocの内容は作成者が管理する MDX です。信頼できないコンテンツにはsandboxプロップで上書きしてください — 上記のセキュリティとsandboxプロップを参照。heightを省略しfullHeightがfalseの場合は、同一オリジンの高さ自動調整が有効になり、後から変わる body のジオメトリとビューポートのリフローが監視されます。固定のheightは正確な値で、オブザーバーを使いません。不透明なサンドボックスではpostMessageによるリサイズを使わないため、固定の高さが必要です。htmlPreview設定フィールドのグローバルリソースは、コンポーネント単位のプロップの前に挿入されます。html、css、jsプロップはインデント付きのテンプレートリテラルに対応しています。先頭の空白はソースコード表示時に自動的に除去されます。クライアントサイドのハイドレーションはコンポーネントラッパーにより自動的に処理されるため、MDX 内で
client:loadディレクティブを付ける必要はありません。externalStyles/externalScriptsは表示時にクライアントサイドで読み込まれます — プレビュー iframe が描画されるときにブラウザが行うネットワークリクエストであり、ビルド時にバンドルされるアセットではありません。上記の外部スタイルシートとスクリプトを参照。srcdoc への挿入順序: preflight リセット(
preflight={false}でない限り)→fullHeightスタイル →externalStyles→externalScripts→head→css。