Understanding the Architecture of Instatic's Visual Editor Canvas
Instatic's visual editor canvas is built as a layered, iframe-based rendering surface that uses per-breakpoint iframes to mirror the published page while maintaining full editing control through isolated gesture handling, selection contexts, and extensible overlay systems.
The visual editor canvas in CoreBunch/Instatic provides a WYSIWYG editing environment that balances fidelity to the final published output with sophisticated design-time interactions. Unlike simple DOM-based editors, the architecture isolates site content within iframe sandboxes while layering editor-specific controls, transforms, and interactions on top. This ensures that CSS cascades, media queries, and runtime scripts behave exactly as they would in production, while the editor maintains granular control over selection, drag-and-drop, and keyboard shortcuts.
Three-Layer Canvas Architecture
The architecture of Instatic's visual editor canvas comprises three distinct vertical slices that handle interaction, transformation, and preview modes.
Root and Interaction Layer
At the top level, src/admin/pages/site/canvas/CanvasRoot.tsx initializes the canvas environment. This component creates the CanvasViewportActionsContext and CanvasSelectionContext providers, mounts the <DndContext> from @dnd-kit/core for drag-and-drop operations, and wraps the entire surface in an ErrorBoundary keyed to the current page. It hosts the UI chrome—including the notch toolbar (CanvasNotch.tsx), breakpoint selector, and mode toggle—while capturing global input events like wheel gestures, pinch-to-zoom, and keyboard shortcuts through useCanvasKeyboardShortcuts.
Transform and Render Layer
src/admin/pages/site/canvas/CanvasTransformLayer.tsx handles the geometric transform pipeline and iframe orchestration. This layer renders a set of <BreakpointFrame> iframes—one per active breakpoint (mobile, tablet, desktop)—each containing a live copy of the site's CSS via CanvasClassCss.tsx and the page's node tree. By mounting each breakpoint in its own iframe, the canvas preserves the exact CSS cascade and media query behavior of the published site, while the editor's --canvas-* design tokens remain isolated in the parent document.
Live Surface Layer
For preview-only workflows, src/admin/pages/site/canvas/CanvasLiveSurface.tsx provides a read-only view. This single iframe displays the fully-published page without attaching pan, zoom, or edit gestures, offering a true-to-life preview toggle that designers can switch to instantly from design mode.
Core Subsystems and Implementation Details
Iframe-Per-Breakpoint Rendering
Every breakpoint receives its own IframeFrameSurface instance. CSS class injection, user stylesheets, and font tokens mount inside each iframe through CanvasClassCss.tsx, ensuring that the editor's stacking context never pollutes the site's global CSS. This isolation allows the canvas to render mobile, tablet, and desktop views simultaneously while maintaining accurate cascade resolution.
Selection and Hover Management
Node selection state lives in the global editor store (selectedNodeId, selectedNodeIds) rather than React component state. The CanvasSelectionContext defined in src/admin/pages/site/canvas/CanvasContexts.tsx provides stable callbacks to child components, while individual NodeRenderer components subscribe directly to store-derived booleans. This architecture ensures that only the affected nodes re-render during click or hover interactions, maintaining performance across large documents.
Drag-and-Drop Resolution
The canvas owns its dedicated DnD context separate from the rest of the admin interface. Drop resolution logic resides in src/admin/pages/site/canvas/canvasDnd.ts, specifically resolveCanvasDropTarget and resolveCanvasPointerInsertionDrop, which are shared with the DOM panel tree. This guarantees parity between canvas drags and side-panel reordering operations, with insertion logic handling both module picker drops and media library assets through canvasInsertionDrop.ts.
Keyboard Shortcuts and Actions
Input handling flows through useCanvasKeyboardShortcuts, which consults the central key-binding registry (@admin/spotlight/keybindings). Actions such as Delete, Duplicate, Copy, Cut, and Paste dispatch to the editor store, supporting multi-node operations batched into single undo steps via mutateActiveTree.
Modals and Overlay System
Several specialized components manage floating UI elements:
- Rename Dialog:
CanvasRenameDialoganduseCanvasRenameDialoghandle inline node renaming. - Context Menu:
CanvasLayerContextMenuopens on right-click, usingclientPointToEditorDocto translate iframe-relative coordinates to the editor document space. - Plugin Overlays:
PluginCanvasOverlayLayerrenders plugin-registered components after the transform layer, ensuring overlays appear above all nodes but only in design mode.
Design Mode Controls
The CanvasNotch component hosts lazy-loaded mode-specific controls. VisualComponentModeControl renders when editing isolated visual components, while TemplateModeControl activates for template-preview workflows. The mode toggle itself lives in src/admin/pages/site/canvas/CanvasModeToggle.tsx, switching between "Design" and "Live" canvas views.
Error Boundaries and Resilience
The canvas implements an ErrorBoundary with location key "canvas" that resets whenever the page or document changes (via resetKeys). This prevents a single broken module or runtime error from crashing the entire editor interface.
Runtime Script Injection
When "Run scripts" is enabled, useRuntimeScriptBuild compiles a single script bundle injected into all breakpoint iframes simultaneously. This avoids per-frame rebuilds and ensures consistent JavaScript execution across all viewport previews.
Data Flow and State Management
The canvas follows a unidirectional data flow:
- Store hydration: The editor store provides the active page via
selectActiveCanvasPage, along with breakpoints, selected node IDs, and user permissions. - Template composition:
CanvasComposition.tscomputes the full template chain for the active document usingresolveEditorWrapperTemplates. - Rendering:
CanvasTransformLayerrenders each breakpoint iframe, injecting compiled class CSS and mounting eitherCanvasLiveSurfaceor the interactive tree renderer. - Mutation: Interaction events invoke store actions (
selectNode,deleteNode,wrapNode) which mutate the immutable page tree throughmutateActiveTree, triggering targeted re-renders of only affected nodes.
Practical Implementation Examples
Developers can extend the canvas through its plugin overlay API or manipulate selection programmatically.
Registering a custom canvas overlay:
// Example: Adding a custom overlay to the canvas
import { useCanvasOverlay } from '@site/hooks/useCanvasOverlay';
function MyOverlay() {
const { canvasRootRef } = useCanvasOverlay(); // gets the root div ref
return (
<div
style={{
position: 'absolute',
top: 10,
left: 10,
background: 'rgba(0,0,0,0.2)',
padding: '4px 8px',
borderRadius: 4,
}}
>
Custom overlay
</div>
);
}
// Register it via a plugin (plugin‑canvas‑overlay)
export const register = (api) => {
api.registerCanvasOverlay(() => <MyOverlay />);
};
Programmatic node selection:
// Example: Selecting a node programmatically (e.g. from a sidebar)
import { useEditorStore } from '@site/store/store';
function selectFirstHeader() {
const page = useEditorStore.getState().selectedPage;
const headerNodeId = Object.values(page.nodes).find(
(n) => n.moduleId === 'base.heading'
)?.id;
if (headerNodeId) useEditorStore.getState().selectNode(headerNodeId, 'replace');
}
Summary
- Instatic's canvas uses a three-layer architecture: Root/Interaction, Transform/Render, and Live Surface, implemented in
CanvasRoot.tsx,CanvasTransformLayer.tsx, andCanvasLiveSurface.tsx. - Per-breakpoint iframes isolate CSS cascades while maintaining accurate media query rendering across viewports.
- Global store subscription minimizes re-renders during selection and hover, with individual
NodeRenderercomponents connecting directly to state. - Shared DnD resolution in
canvasDnd.tsensures consistent drag-and-drop behavior between the canvas and DOM panel. - Plugin overlay system via
PluginCanvasOverlayLayerallows third-party extensions to render above the canvas content in design mode. - Error boundaries and runtime script injection provide resilience and accurate JavaScript execution across all breakpoint previews.
Frequently Asked Questions
How does Instatic's canvas handle CSS isolation between the editor and the site content?
The canvas renders each breakpoint in its own iframe via CanvasTransformLayer.tsx, injecting site-specific CSS through CanvasClassCss.tsx. This keeps the site's global styles scoped within the iframe while the editor's --canvas-* tokens and UI chrome remain in the parent document, preventing cascade pollution.
What drives the selection system in the Instatic canvas?
Selection state lives in the central editor store (selectedNodeId, selectedNodeIds), not in React local state. The CanvasSelectionContext provides callback stability, while individual node components subscribe to boolean selectors from the store, ensuring that only the clicked or hovered nodes re-render rather than the entire tree.
Can plugins add visual elements to the Instatic canvas?
Yes. Plugins can register overlay components through the PluginCanvasOverlayLayer system. These overlays render after the transform layer in design mode, appearing above the page content but below modal dialogs, and receive the canvas root ref via useCanvasOverlay for positioning.
How does the canvas maintain undo/redo parity between the visual canvas and the DOM panel?
Both interfaces share the same drop resolution logic exported from src/admin/pages/site/canvas/canvasDnd.ts, specifically resolveCanvasDropTarget and resolveCanvasPointerInsertionDrop. Store actions like mutateActiveTree batch mutations into single undo steps regardless of whether the interaction originated from the canvas or the side panel.
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 →