How the Visual Editor Canvas Renders Per-Breakpoint Iframes in Instatic
Instatic renders each responsive breakpoint as an isolated iframe to guarantee accurate CSS media query evaluation and JavaScript sandboxing, with BreakpointFrame.tsx handling iframe creation and CanvasRoot.tsx orchestrating multiple simultaneous views via a Redux-managed state.
Instatic is an open-source visual site builder that employs a multi-iframe architecture for its editing canvas. Unlike traditional responsive preview modes that simply resize a single viewport, the visual editor canvas renders per-breakpoint iframes to provide authentic device emulation. This design ensures that CSS media queries, element dimensions, and JavaScript window measurements behave exactly as they would on actual target devices, while keeping plugin execution safely isolated from the host application.
The Per-Breakpoint Iframe Architecture
The core philosophy behind Instatic's canvas is complete isolation. Rather than simulating breakpoints through CSS transforms or container queries on a shared DOM, the editor spawns a distinct <iframe> for every active breakpoint (Desktop, Tablet, Mobile, etc.). This architecture delivers three critical advantages:
- True media query evaluation: Each iframe maintains its own viewport dimensions, triggering genuine CSS media query breakpoints rather than emulated ones.
- Sandboxed execution: Plugin code runs inside a QuickJS-WASM sandbox within each iframe, preventing malicious or buggy scripts from crashing the main editor interface.
- Concurrent multi-view: Authors can view and edit Desktop, Tablet, and Mobile layouts simultaneously, with changes propagating instantly across all frames.
Core Rendering Pipeline
Breakpoint Selection State
The rendering process begins with user interaction in src/admin/pages/site/canvas/BreakpointSelectionOverlay.tsx. This component presents the breakpoint toggles and dispatches Redux actions to update the activeBreakpointId slice. When a user activates a new breakpoint, the store notifies the canvas root, triggering a re-render that instantiates a new BreakpointFrame for the selected device width.
Frame Orchestration
The src/admin/pages/site/canvas/CanvasRoot.tsx component acts as the conductor. It subscribes to the breakpoint state and renders a BreakpointFrame for every active ID. These frames are wrapped in CanvasTransformLayer.tsx, which applies pan-and-zoom transformations to the entire canvas surface without interfering with individual iframe contents.
Document Initialization and CSS Injection
Each BreakpointFrame defined in src/admin/pages/site/canvas/BreakpointFrame.tsx generates a clean HTML document via the srcDoc attribute:
// src/admin/pages/site/canvas/BreakpointFrame.tsx
export const BreakpointFrame = ({ breakpointId }: { breakpointId: string }) => {
const { width, height } = BREAKPOINTS[breakpointId];
const srcDoc = `
<!doctype html>
<html>
<head><style id="instatic-css"></style></head>
<body><div id="instatic-root"></div></body>
</html>
`;
return (
<iframe
title={`Breakpoint ${breakpointId}`}
srcDoc={srcDoc}
width={width}
height={height}
sandbox="allow-scripts allow-same-origin"
/>
);
};
Once mounted, src/admin/pages/site/canvas/DocumentSwitcher.tsx coordinates with IframeFrameSurface.tsx to populate the iframe. It injects the site’s HTML structure and the compiled CSS bundle (siteCssBundle.ts) generated by src/core/framework. The stylesheet contains both default rules and per-breakpoint overrides, ensuring the iframe renders the correct visual styles for its specific dimensions.
Runtime Bridge and Sandbox
To enable communication between the host React application and the isolated iframe, src/admin/pages/site/canvas/RuntimeScriptInjector.tsx injects the editor runtime script (/runtime/editor.js):
// src/admin/pages/site/canvas/RuntimeScriptInjector.tsx
export const RuntimeScriptInjector = ({ iframe }: { iframe: HTMLIFrameElement }) => {
useEffect(() => {
const doc = iframe.contentDocument!;
const script = doc.createElement('script');
script.src = '/runtime/editor.js';
doc.body.appendChild(script);
// Exposes window.__INSTATIC_EDITOR for bidirectional communication
}, [iframe]);
return null;
};
This script registers the window.__INSTATIC_EDITOR global object, establishing a bridge for live updates, undo/redo operations, and UI event forwarding. For plugin execution, src/admin/pages/site/canvas/ModuleSandboxFrame.tsx initializes a QuickJS sandbox inside the iframe, compiling and running user-provided code without granting access to the parent window or other breakpoint frames.
Live Editing Synchronization
When an author edits content—such as modifying text in a heading—the change flows through the Redux store and reaches src/admin/pages/site/canvas/NodeRenderer.tsx. This component uses React Portals to render the updated node into every active iframe simultaneously:
// src/admin/pages/site/canvas/NodeRenderer.tsx
export const NodeRenderer = ({ nodeId }: { nodeId: string }) => {
const node = useSelector((s) => selectNodeById(s, nodeId));
const activeFrames = useSelector((s) => s.canvas.activeBreakpointIds);
return (
<>
{activeFrames.map((bp) => (
<Portal
key={bp}
container={iframeRoots[bp].getElementById('instatic-root')}
>
<YourComponent props={node.props} />
</Portal>
))}
</>
);
};
Because each iframe hosts an identical React component tree synchronized to the same Redux state, mutations appear instantly across all breakpoint views. The inlineEditSlice manages these transient editing states, ensuring that draft content remains consistent whether viewed in the Desktop or Mobile iframe.
Implementation Examples
Dispatching Breakpoint Changes
To programmatically switch the active breakpoint, components dispatch actions to the breakpoint slice:
import { useDispatch, useSelector } from 'react-redux';
import { setActiveBreakpoint } from '@/store/slices/breakpointSlice';
import { BREAKPOINTS } from '@/core/page-tree/breakpoint';
export const BreakpointSwitcher = () => {
const dispatch = useDispatch();
const active = useSelector((s) => s.breakpoint.activeId);
return (
<div className="breakpoint-switcher">
{Object.entries(BREAKPOINTS).map(([id, cfg]) => (
<button
key={id}
className={active === id ? 'active' : ''}
onClick={() => dispatch(setActiveBreakpoint(id))}
>
{cfg.label}
</button>
))}
</div>
);
};
Styling Injection per Frame
The CSS injection mechanism ensures each iframe receives the appropriate stylesheet without polluting the host application:
// Inside RuntimeScriptInjector or DocumentSwitcher
const style = iframe.contentDocument!.getElementById('instatic-css') as HTMLStyleElement;
style.textContent = siteCssBundle; // Contains [data-breakpoint="mobile"] qualifiers
Summary
- Instatic's visual editor canvas renders per-breakpoint iframes to provide authentic device preview and sandboxed execution.
BreakpointFrame.tsxcreates isolated iframe elements sized to specific breakpoint dimensions using thesrcDocattribute.CanvasRoot.tsxorchestrates multiple frames simultaneously, whileCanvasTransformLayer.tsxhandles canvas-level navigation.- Document injection occurs via
DocumentSwitcher.tsxandIframeFrameSurface.tsx, loading HTML and the CSS bundle (siteCssBundle.ts) into each frame. - Runtime communication is established by
RuntimeScriptInjector.tsx, which injects the bridge script exposingwindow.__INSTATIC_EDITOR. - Plugin isolation is enforced by
ModuleSandboxFrame.tsxusing a QuickJS-WASM sandbox. - Live synchronization across all active frames is achieved through Redux state management and React Portals in
NodeRenderer.tsx.
Frequently Asked Questions
Why does Instatic use separate iframes for each breakpoint instead of resizing a single viewport?
Separate iframes ensure that CSS media queries evaluate against the correct viewport dimensions and that JavaScript relying on window.innerWidth returns accurate values. A single resized viewport would require polyfilling or emulation, which often fails with complex media queries, container queries, or third-party scripts. Additionally, iframes provide natural process isolation for plugins, preventing code in one breakpoint view from interfering with another.
How does CSS isolation work across breakpoint frames?
Each iframe receives the complete CSS bundle generated by src/core/framework, which includes both default styles and breakpoint-specific rules. The styles are injected into a <style id="instatic-css"> element within the iframe's shadow DOM or document head. Because the styles reside inside the iframe, they cannot leak into the host application or other breakpoint frames, and media queries trigger based on the iframe's intrinsic width rather than the browser window.
What role does the QuickJS sandbox play in the per-breakpoint iframe?
The QuickJS sandbox, hosted in src/admin/pages/site/canvas/ModuleSandboxFrame.tsx, executes user-provided plugin code inside the iframe without granting access to the parent window or global scope. This prevents plugins from manipulating the editor's React state, accessing unauthorized APIs, or causing cross-frame side effects. Each breakpoint iframe maintains its own sandbox instance, ensuring that plugin state remains isolated per viewport.
How do live edits propagate to all active breakpoint frames simultaneously?
When a content mutation occurs, the inlineEditSlice updates the Redux store, which triggers a re-render in src/admin/pages/site/canvas/NodeRenderer.tsx. This component maps over the activeBreakpointIds array and uses React Portals to render the updated component tree into the instatic-root container of every active iframe. Since all frames subscribe to the same Redux state, the edit appears instantly across Desktop, Tablet, and Mobile views without requiring separate network requests or recompilation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →