How Visual Components Handle Typed Parameters and Slot Synchronization in Instatic

Visual Components in Instatic use a TypeBox schema for typed parameters and a dedicated slot synchronization module to keep page instances in sync with component definition changes.

Visual Components (VCs) are the reusable building blocks of Instatic's visual editing system. They expose a strongly-typed parameter interface through a declarative schema, while slot-based composition allows nested content. Understanding how typed parameters and slot synchronization work together is essential for anyone extending or debugging the Instatic editor.

Typed Parameters Through TypeBox Schemas

Every Visual Component is defined by a TypeBox schema located in src/core/visualComponents/schemas.ts. This schema establishes the contract between component authors, the editor UI, and the runtime engine.

The VCParam Schema

Each parameter in a VC's params array conforms to the VCParam schema. The supported types include:

  • string – plain text input
  • number – numeric values with optional min/max constraints
  • boolean – toggle switches
  • url – validated external links
  • enum – dropdown selections with predefined options
  • color – color picker values
  • image – asset references
  • richText – formatted content blocks
  • slot – nested content areas

During parsing, the parseVisualComponent function validates raw data against this schema. Unknown types gracefully fall back to string, while helper logic normalizes default values, enum options, and required flags. The output is a typed VCParam[] that both the editor and runtime can rely on without additional runtime checks.

import { VisualComponentSchema } from '@core/visualComponents/schemas'

const myButtonVC = {
  id: 'btn-01',
  name: 'Button',
  tree: { /* VC node tree */ },
  params: [
    { id: 'p1', name: 'label', type: 'string', defaultValue: 'Click me', required: true },
    { id: 'p2', name: 'href',  type: 'url',    defaultValue: '', required: false },
    { id: 'p3', name: 'icon',  type: 'slot',   defaultValue: null, required: false },
  ],
  classIds: [],
  createdAt: Date.now(),
}

How Slots Are Represented in the VC Tree

Slot-type parameters have a special representation. Unlike other parameters, slots do not appear as entries in the params array with storage semantics. Instead, each slot is represented by a base.slot-outlet node embedded directly in the VC's own node tree.

The props.slotName property on each base.slot-outlet node serves as the canonical identifier for that slot. This design separates slot definitions (what slots exist) from slot instances (what content occupies them on a specific page).

Slot Synchronization When Definitions Change

When a Visual Component definition changes—adding a new slot, renaming an existing one, or removing a slot—the editor must reconcile all existing VC references on pages. This is handled by the slot synchronization module in src/core/visualComponents/slotSync.ts.

Extracting Slot Names from the VC Definition

The collectSlotOutletNames function walks the VC's flat node map and extracts all unique slot names from base.slot-outlet nodes. This produces the authoritative set of slots that should exist for any reference to this VC.

Computing Required Synchronization Operations

The syncSlotInstances function performs a pure comparison between the VC's slot definitions and the existing base.slot-instance children of a VC reference. It returns a SyncResult describing the minimal operations needed to bring the page tree into alignment:

Operation Trigger Implementation
Insert New slot in VC not present in VC reference Create locked base.slot-instance node with matching slotName
Rename Slot name changed in VC definition Update props.slotName on existing base.slot-instance
Delete Slot removed from VC definition Remove orphaned base.slot-instance nodes including all subtrees

Applying Synchronization Results

The applySlotSyncResult function executes these operations within a Mutative recipe. It mutates the page tree draft by adding new nodes, applying rename operations, deleting orphans, and finally re-ordering the VC reference's children array to match the VC's slot definition order.

import { syncSlotInstances, applySlotSyncResult } from '@core/visualComponents/slotSync'
import { mutateActiveTree } from '@admin/pages/site/store/slices/site/helpers'

// vcRefNode = node representing the VC reference on the page
// vc = the VisualComponent definition (as parsed above)
// treeNodes = mutable draft of the page's node map

mutateActiveTree((draft) => {
  const result = syncSlotInstances(vcRefNode, vc, draft)
  applySlotSyncResult(draft, result, vcRefNode.id)
})

Resolving Parameter Origins

The origin lookup system in src/core/visualComponents/origin.ts connects a VC parameter back to the specific node that provides its binding. This enables the editor to highlight source nodes and validate bindings.

For non-slot parameters, findParamOrigin scans node.propBindings for a matching paramId. This reveals which node property is bound to the parameter.

For slot parameters, the system locates the base.slot-outlet node whose props.slotName matches the parameter name. This bridges the semantic parameter name to the structural node in the VC tree.

import { findParamOrigin } from '@core/visualComponents/origin'

const origin = findParamOrigin(vc, 'p1')
if (origin) {
  console.log('Parameter lives on node', origin.nodeId, 'prop key', origin.propKey)
}

Integration with the Editor Store

Slot synchronization is triggered automatically when VC definitions change. The visualComponentsSlice.ts file integrates this into the Redux store, calling syncSlotInstances whenever parameters are modified and ensuring all page instances stay consistent.

Summary

  • TypeBox schemas in src/core/visualComponents/schemas.ts enforce typed parameters with graceful fallbacks for unknown types
  • Slot definitions exist as base.slot-outlet nodes in the VC tree rather than parameter entries
  • Slot synchronization (src/core/visualComponents/slotSync.ts) uses pure SyncResult operations for insert, rename, and delete actions
  • Origin resolution (src/core/visualComponents/origin.ts distinguishes binding lookup for slot vs. non-slot parameters
  • Mutative recipes ensure all tree mutations remain traceable and undoable

Frequently Asked Questions

What happens when a Visual Component adds a new slot parameter?

The editor detects the change in the VC definition and triggers syncSlotInstances. For each VC reference on the page, it creates a new locked base.slot-instance node with the appropriate slotName. Existing page content remains untouched, and the new slot appears empty and ready for content.

How does Instatic handle unknown or invalid parameter types?

During parseVisualComponent, any type not recognized in the VCParam schema falls back to string. This prevents crashes while maintaining forward compatibility. Component authors can see the fallback behavior in the editor and correct the type definition.

Can slot instances contain their own nested Visual Components?

Yes. Slot instances are full nodes in the page tree with their own children array. They can contain any valid node type, including other VC references. When a slot is deleted during synchronization, the entire subtree—including any nested components—is removed.

What's the difference between base.slot-outlet and base.slot-instance?

base.slot-outlet appears in the VC definition and declares that the component accepts content in a named slot. base.slot-instance appears in page trees as a child of a VC reference, representing the actual container where editors place content for that slot.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →