How Visual Components Work in Instatic: Typed Parameters and Named Slots Explained

Visual Components in Instatic use TypeBox schemas to define strongly-typed parameters and declare named slots via base.slot-outlet nodes, with automatic synchronization materializing base.slot-instance children when instantiated.

In the CoreBunch/Instatic repository, Visual Components (VCs) provide a reusable UI architecture that separates component definitions from their runtime instances. The system employs TypeBox schemas in src/core/visualComponents/schemas.ts to enforce type safety on configuration parameters while using a node-based slot system for content composition. Understanding how typed parameters and named slots interact is essential for building robust, composable components in this flat-map node tree architecture.

Declaring Typed Parameters with TypeBox Schemas

Every Visual Component declares its configurable inputs through a params array defined by VCParamSchema (lines 66‑80 of src/core/visualComponents/schemas.ts). These schemas serve as the single source of truth, with TypeScript types derived via Static<typeof …> rather than parallel interfaces.

The VCParamSchema Structure

Each parameter object follows a strict structure validated at runtime:

  • id – A stable nano‑id that survives renames across component versions.
  • name – A human‑readable identifier unique within the VC.
  • type – One of the supported VCParamType literals.
  • defaultValue – The fallback value when users provide no input.
  • required – Boolean flag forcing the editor to demand a value.
  • enumOptions – Array of allowed strings when type is enum.

Supported Parameter Types

The VCParamTypeSchema (lines 25‑34) defines the union of allowable types:

  • string – Plain text input.
  • number – Numeric values.
  • boolean – True/false toggles.
  • url – Validated URL strings.
  • enum – Selection from predefined options.
  • color – Color picker values.
  • image – Asset references.
  • richText – Formatted content blocks.
  • slot – Special type for nested slot parameters.

When loading from the database, the parseVisualComponent function (lines 90‑100) automatically falls back unknown types to string and supplies default values, ensuring malformed rows never crash the editor.

Defining Named Slots with Slot Outlets

Unlike parameters, slots are not declared in the params array. Instead, slots are materialized by nodes with moduleId: 'base.slot-outlet' placed inside the VC’s own node tree.

Slot Outlet Nodes

Inside the component definition’s tree, a slot outlet is a standard node with a slotName property:

{
  id: 'slotOutlet',
  moduleId: 'base.slot-outlet',
  props: { slotName: 'icon' },
  children: [],
}

This declaration exposes a slot named icon that consumers can fill with their own content.

Extracting Slot Names

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

Synchronizing Slot Instances at Runtime

When a VC is dropped onto a page, the editor creates a base.visual-component-ref node pointing to the component definition. The system then synchronizes the reference’s children to match the VC’s declared slots.

The syncSlotInstances Function

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

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

Applying Slot Synchronization

The applySlotSyncResult function (lines 66‑100) executes the mutations inside a Mutative producer:

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

Each new base.slot-instance node is created with locked: true, preventing users from accidentally deleting or reordering these structural children while allowing content insertion within them.

Rendering Visual Components with Slots

During publishing, the renderer walks the page tree and handles VC references specially.

Matching Slot Instances to Outlets

In src/core/publisher/renderVisualComponentRef.ts (line 87), the renderer builds a Map<string, BaseNode> called slotInstancesByName by iterating over the ref node’s children and collecting all base.slot-instance entries. When the walker encounters a base.slot-outlet node in the VC’s internal tree, it looks up the matching slot instance by name and injects the consumer’s children into that position.

As noted in src/core/publisher/renderNode.ts (line 250), slot‑instance children are consumed by the VC during this process, producing the final rendered output.

Summary

  • TypeBox schemas in src/core/visualComponents/schemas.ts provide the single source of truth for VC parameters, supporting types from string to richText with automatic fallback for unknown types.
  • Named slots are declared via base.slot-outlet nodes in the component tree, extracted by collectSlotOutletNames in slotSync.ts.
  • Automatic synchronization via syncSlotInstances and applySlotSyncResult materializes locked base.slot-instance children when a VC is instantiated, keeping the reference node synchronized with the component definition.
  • Runtime rendering matches slot instances to outlets in renderVisualComponentRef.ts, injecting consumer content into the correct positions of the VC’s markup.

Frequently Asked Questions

What parameter types are supported in Instatic Visual Components?

Instatic supports string, number, boolean, url, enum, color, image, richText, and slot types, as defined by VCParamTypeSchema in src/core/visualComponents/schemas.ts (lines 25‑34). Each type enforces specific validation rules in the editor, with enum requiring an accompanying enumOptions array.

How does Instatic handle unknown or malformed parameter types?

The parseVisualComponent function (lines 90‑100 of schemas.ts) implements a tolerant parser that automatically falls back unknown types to string and supplies default values for missing fields. This guarantees that corrupted database rows never crash the editor or runtime.

What is the difference between a slot-outlet and a slot-instance?

A base.slot-outlet exists inside the Visual Component definition itself and declares that the component accepts content in a specific named slot. A base.slot-instance is a child node of a visual-component-ref on a page that actually holds the consumer’s content; these are auto‑created and locked by the synchronization system to match the outlets declared in the definition.

How does the publisher render content into Visual Component slots?

During rendering, renderVisualComponentRef.ts (line 87) builds a lookup map of slot instances by name from the reference node’s children. When the renderer encounters a base.slot-outlet in the VC’s tree, it retrieves the corresponding slot instance and injects that instance’s children into the outlet’s position, as cross‑referenced in renderNode.ts (line 250).

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 →