diff --git a/frontend/public/llms.txt b/frontend/public/llms.txt
index 9fef57d..6b14104 100644
--- a/frontend/public/llms.txt
+++ b/frontend/public/llms.txt
@@ -598,6 +598,26 @@ iframe.contentWindow.postMessage(
Accepted values for `theme`: `"dark"`, `"light"`. Other messages are ignored.
+### Auto-resize via postMessage
+
+The embed reports its content height to the parent page whenever the layout changes (initial render, simulation results, expanded cells). The parent listens for `spicebook-resize` messages and updates the iframe height:
+
+```javascript
+window.addEventListener('message', function(e) {
+ if (e.data && e.data.type === 'spicebook-resize') {
+ document.getElementById('spicebook-{notebook_id}').style.height = e.data.height + 'px';
+ }
+});
+```
+
+The message payload:
+
+```json
+{ "type": "spicebook-resize", "height": 1842 }
+```
+
+`height` is `document.documentElement.scrollHeight` in pixels. The embed sends this message on every `ResizeObserver` callback, deduplicated to only fire when height actually changes. The **Embed** button's generated snippet includes this listener automatically.
+
### Discovering the embed snippet
-In the notebook editor UI, the **Embed** button in the toolbar opens a popover with a ready-to-copy iframe snippet and a theme toggle.
+In the notebook editor UI, the **Embed** button in the toolbar opens a popover with a ready-to-copy iframe snippet (including auto-resize listener) and a theme toggle.
diff --git a/frontend/src/components/embed/EmbedViewer.tsx b/frontend/src/components/embed/EmbedViewer.tsx
index cf1339e..c7764a5 100644
--- a/frontend/src/components/embed/EmbedViewer.tsx
+++ b/frontend/src/components/embed/EmbedViewer.tsx
@@ -38,6 +38,26 @@ export default function EmbedViewer({ notebookId, initialTheme }: EmbedViewerPro
return () => window.removeEventListener('message', handleMessage);
}, []);
+ // Report content height to parent frame for auto-resize
+ useEffect(() => {
+ if (typeof window === 'undefined' || window.parent === window) return;
+
+ let lastHeight = 0;
+ function reportHeight() {
+ const height = document.documentElement.scrollHeight;
+ if (height !== lastHeight) {
+ lastHeight = height;
+ window.parent.postMessage({ type: 'spicebook-resize', height }, '*');
+ }
+ }
+
+ const observer = new ResizeObserver(reportHeight);
+ observer.observe(document.body);
+ reportHeight();
+
+ return () => observer.disconnect();
+ }, []);
+
// Keep a ref to notebook for use inside handleRun to avoid stale closures
const notebookRef = useRef(notebook);
notebookRef.current = notebook;
diff --git a/frontend/src/components/notebook/toolbar/EmbedDialog.tsx b/frontend/src/components/notebook/toolbar/EmbedDialog.tsx
index aa0f5b4..f99452c 100644
--- a/frontend/src/components/notebook/toolbar/EmbedDialog.tsx
+++ b/frontend/src/components/notebook/toolbar/EmbedDialog.tsx
@@ -14,12 +14,23 @@ export function EmbedDialog() {
const origin = typeof window !== 'undefined' ? window.location.origin : '';
+ const iframeId = `spicebook-${notebookId}`;
+
const snippet = ``;
+>
+`;
function handleCopy() {
navigator.clipboard.writeText(snippet).then(() => {
@@ -72,7 +83,7 @@ export function EmbedDialog() {