# How the Visual Editor Canvas Renders Per-Breakpoint Iframes in Instatic

> Discover how Instatic's visual editor renders per-breakpoint iframes for accurate CSS media query evaluation and JavaScript sandboxing. Learn about its innovative approach to responsive design.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-29

---

**Instatic renders each responsive breakpoint as an isolated iframe to guarantee accurate CSS media query evaluation and JavaScript sandboxing, with [`BreakpointFrame.tsx`](https://github.com/CoreBunch/Instatic/blob/main/BreakpointFrame.tsx) handling iframe creation and [`CanvasRoot.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/canvas/BreakpointFrame.tsx) generates a clean HTML document via the `srcDoc` attribute:

```typescript
// 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`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/canvas/DocumentSwitcher.tsx) coordinates with [`IframeFrameSurface.tsx`](https://github.com/CoreBunch/Instatic/blob/main/IframeFrameSurface.tsx) to populate the iframe. It injects the site’s HTML structure and the compiled CSS bundle ([`siteCssBundle.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/canvas/RuntimeScriptInjector.tsx) injects the editor runtime script ([`/runtime/editor.js`](https://github.com/CoreBunch/Instatic/blob/main//runtime/editor.js)):

```typescript
// 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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/canvas/NodeRenderer.tsx). This component uses React Portals to render the updated node into every active iframe simultaneously:

```typescript
// 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:

```typescript
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:

```typescript
// 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.tsx`](https://github.com/CoreBunch/Instatic/blob/main/BreakpointFrame.tsx)** creates isolated iframe elements sized to specific breakpoint dimensions using the `srcDoc` attribute.
- **[`CanvasRoot.tsx`](https://github.com/CoreBunch/Instatic/blob/main/CanvasRoot.tsx)** orchestrates multiple frames simultaneously, while **[`CanvasTransformLayer.tsx`](https://github.com/CoreBunch/Instatic/blob/main/CanvasTransformLayer.tsx)** handles canvas-level navigation.
- **Document injection** occurs via [`DocumentSwitcher.tsx`](https://github.com/CoreBunch/Instatic/blob/main/DocumentSwitcher.tsx) and [`IframeFrameSurface.tsx`](https://github.com/CoreBunch/Instatic/blob/main/IframeFrameSurface.tsx), loading HTML and the CSS bundle ([`siteCssBundle.ts`](https://github.com/CoreBunch/Instatic/blob/main/siteCssBundle.ts)) into each frame.
- **Runtime communication** is established by [`RuntimeScriptInjector.tsx`](https://github.com/CoreBunch/Instatic/blob/main/RuntimeScriptInjector.tsx), which injects the bridge script exposing `window.__INSTATIC_EDITOR`.
- **Plugin isolation** is enforced by [`ModuleSandboxFrame.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ModuleSandboxFrame.tsx) using a QuickJS-WASM sandbox.
- **Live synchronization** across all active frames is achieved through Redux state management and React Portals in [`NodeRenderer.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.