How Instatic Visual Components Use Typed Parameters and Named Slots: A Complete Guide

Instatic Visual Components declare typed parameters via TypeBox schemas and define named slots through base.slot-outlet nodes, which the editor automatically materializes as locked base.slot-instance children when a component is placed on a page.

Instatic, an open-source visual site builder maintained by CoreBunch, treats Visual Components (VCs) as reusable UI elements backed by a flat-map node tree. Understanding how Instatic Visual Components use typed parameters and named slots is essential for building robust, type-safe modules that survive editor mutations and render correctly at publish time.

Declaring Typed Parameters with TypeBox Schemas

Every Visual Component in Instatic describes its configuration surface through TypeBox schemas defined in src/core/visualComponents/schemas.ts. These schemas serve as the single source of truth, with TypeScript types derived via Static<typeof …> and no parallel interfaces maintained.

The VCParamSchema Structure

Parameters are declared in the params array of a VisualComponent. Each entry follows VCParamSchema (lines 66‑80) and requires:

  • id: A stable nano-id that persists across renames.
  • name: A human-readable identifier unique within the VC.
  • type: A literal from VCParamTypeSchema (lines 25‑34): string, number, boolean, url, enum, color, image, richText, or slot.
  • defaultValue: The fallback used when no value is provided.
  • required: Boolean flag forcing the editor to demand a value.
  • enumOptions: String array for enum types.
// Example parameter definition inside a VC schema
{
  id: 'param_123',
  name: 'label',
  type: 'string',
  defaultValue: 'Click me',
  required: true
}

Tolerant Parsing for Robustness

When loading from the database, the parseVisualComponent function (lines 90‑100) enforces safety. Unknown parameter types automatically fall back to string, and missing values receive their defaultValue. This guarantees that malformed rows never crash the editor, preserving the integrity of Instatic Visual Components across schema migrations.

Defining Named Slots with Slot Outlets

Unlike parameters, named slots are not declared in the params array. Instead, slots materialize through special nodes inside the VC’s own tree.

Slot Declaration in the Component Tree

A slot is declared by placing a node with moduleId: 'base.slot-outlet' anywhere in the component’s node tree. The slotName prop on this node defines the slot’s public identity.

// Inside the VC tree definition
slotOutlet: {
  id: 'slotOutlet',
  moduleId: 'base.slot-outlet',
  props: { slotName: 'icon' },  // Declares a slot named "icon"
  children: [],
}

Extracting Slot Names from the Tree

The helper collectSlotOutletNames in src/core/visualComponents/slotSync.ts (lines 26‑70) walks the VC tree and extracts slotName values from every base.slot-outlet. The first appearance of a name wins, producing an ordered list that serves as the authoritative source of which slots the VC exposes.

Synchronizing Slot Instances on the Page

When a user drops a Visual Component onto a page, the editor creates a base.visual-component-ref node pointing to the VC definition. The system then ensures the ref’s children match the VC’s slot declarations.

The syncSlotInstances Algorithm

The syncSlotInstances function (lines 135‑148 of slotSync.ts) performs a pure, side-effect-free comparison:

  1. Collects current slot names from the VC via collectSlotOutletNames.
  2. Matches existing base.slot-instance children of the ref by name (or position for renamed slots).
  3. Generates insert, rename, and delete operations to align the ref’s children with the VC’s current slot list.
  4. Returns a SyncResult containing the operations, newly created nodes, and the final orderedChildIds.

Applying Changes with applySlotSyncResult

The applySlotSyncResult function (lines 66‑100) executes the generated plan inside a Mutative producer. It inserts new base.slot-instance nodes, applies renames, and removes stale slots. Each new slot-instance is created with locked: true, preventing users from accidentally deleting or reordering these structural children.

// Inside a Mutative producer (e.g., siteSlice)
import { syncSlotInstances, applySlotSyncResult } from '@core/visualComponents/slotSync';

const syncResult = syncSlotInstances(vcRefNode, myButtonVC, state.page.nodes);
applySlotSyncResult(state.page.nodes, syncResult, vcRefNode.id);

Runtime Rendering of Slots

During publishing, the renderer pairs consumer-provided content with the VC’s internal outlets.

Matching Slot Instances to Outlets

In src/core/publisher/renderVisualComponentRef.ts (line 87), the renderer builds a slotInstancesByName map from the ref’s base.slot-instance children. When walking the VC’s tree, each base.slot-outlet node looks up its slotName in this map. The children of the matching slot-instance replace the outlet in the final output, as noted in src/core/publisher/renderNode.ts (line 250).

// Excerpt from renderVisualComponentRef.ts
const slotInstancesByName = new Map<string, BaseNode>();
for (const childId of vcRefNode.children) {
  const child = tree.nodes[childId];
  if (child?.moduleId === 'base.slot-instance') {
    slotInstancesByName.set(child.props.slotName, child);
  }
}

// Later, when encountering a slot-outlet
if (node.moduleId === 'base.slot-outlet') {
  const slotInst = slotInstancesByName.get(node.props.slotName);
  // Render slotInst.children in place of the outlet
}

Summary

  • TypeBox schemas in src/core/visualComponents/schemas.ts define typed parameters via VCParamSchema, supporting types from string to richText with automatic fallback handling in parseVisualComponent.
  • Named slots are declared implicitly by base.slot-outlet nodes in the VC tree, extracted by collectSlotOutletNames in slotSync.ts.
  • Automatic synchronization occurs through syncSlotInstances and applySlotSyncResult, which materialize locked base.slot-instance children on every base.visual-component-ref.
  • Runtime rendering maps slot-instances to outlets in renderVisualComponentRef.ts, injecting consumer content into the correct positions.

Frequently Asked Questions

How do I add a new typed parameter to an Instatic Visual Component?

Extend the params array in your component definition with an object conforming to VCParamSchema. Specify the type using one of the literals defined in VCParamTypeSchema (lines 25‑34), provide a defaultValue, and ensure the id uses a stable nano-id. The tolerant parser parseVisualComponent (lines 90‑100) will handle backward compatibility automatically.

What happens if I rename a slot in my Visual Component?

When you change the slotName prop of a base.slot-outlet node, syncSlotInstances detects the mismatch during the next editor session. It generates a rename operation that updates the corresponding base.slot-instance child’s slotName prop, preserving the existing children (content) while updating the identifier.

Can users delete slot instances from a Visual Component reference?

No. The applySlotSyncResult function creates all base.slot-instance nodes with locked: true. This flag prevents users from deleting or reordering these structural children in the editor, ensuring the VC’s slot contract remains intact. Users can only edit the content inside the slot instances.

Where is the single source of truth for Visual Component types?

The src/core/visualComponents/schemas.ts file contains the TypeBox schemas (VisualComponentSchema, VCParamSchema, VCParamTypeSchema) that serve as the canonical definitions. TypeScript types like VisualComponent and VCParam are derived using Static<typeof …>, ensuring runtime validation and static analysis never drift apart.

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 →