How Instatic Visual Components Work with Typed Parameters and Slots

Instatic Visual Components combine TypeBox schemas for strongly-typed parameters with an automatic slot-instance generation system, enabling composable, type-safe UI building blocks that validate data at every boundary from editor to runtime.

In the CoreBunch/Instatic repository, Visual Components (VCs) function as first-class citizens within the page-tree architecture, operating alongside standard pages and layout nodes. Each VC declares a strict TypeBox schema that governs its public parameters and slot definitions, ensuring that developers receive full TypeScript type safety while the editor automatically manages content insertion points.

Declaring Typed Parameters with TypeBox

Every Visual Component in Instatic begins with a TypeBox schema that rigidly defines the shape of its configurable properties. In src/core/visualComponents/schemas.ts, the system provides base schemas like BaseVisualComponentSchema that all VCs extend.

When defining a component, you create a parameter schema using TypeBox's Type.Object:

import { Type } from '@core/typebox';

export const MyButtonParamsSchema = Type.Object({
  label: Type.String({ title: 'Button label' }),
  disabled: Type.Boolean({ default: false, title: 'Disabled?' })
});

The editor validates all incoming data through validateParams, which internally utilizes safeParseJson to guarantee type safety before any node insertion occurs. This validation happens in src/core/visualComponents/instantiate.ts during the instantiateVc call, ensuring that malformed data never enters the tree.

Automatic Slot Instance Generation

VCs achieve composability through named slots defined via SlotParamSchema. When a schema includes a slots object, the system automatically manages content insertion points.

In src/core/visualComponents/slotSync.ts, the syncSlotInstances function automatically creates slot-instance child nodes (of type base.slot-instance) for each declared slot when a VC is instantiated:

export const MyButtonSlotSchema = Type.Object({
  icon: Type.Optional(Type.Any())
});

export const MyButtonSchema = Type.Intersect([
  MyButtonParamsSchema,
  Type.Object({ slots: MyButtonSlotSchema })
]);

These slot-instance nodes become standard page-tree nodes that serve as insertion points for user-provided content. The architecture treats slots as regular tree children, maintaining consistency with Instatic's generic node management system.

The Instantiation Pipeline

When a user drops a VC onto the canvas, the editor executes a strict instantiation pipeline centered in src/core/visualComponents/instantiate.ts. The instantiateVc function orchestrates this process through four distinct phases:

  1. Parameter validation against the TypeBox schema
  2. Slot instance creation via syncSlotInstances
  3. Node construction with validated parameters and generated children
  4. Tree registration through mutateActiveTree

This pipeline is tree-agnostic, meaning identical code paths handle mutations for pages, components, and layout trees. The mutateActiveTree API registers the new node atomically, ensuring the editor state remains consistent whether you're working with a root page or a nested component.

Utilities in src/core/visualComponents/vcRefs.ts provide resolution mechanisms for referencing VCs by ID and retrieving their source definitions throughout this pipeline.

Runtime Slot Resolution

At publish time, the rendering engine must link slot instances to their corresponding outlets. In src/core/visualComponents/origin.ts, the system tracks the origin of each VC instance—its source definition and slot configuration.

The publisher walks the tree and, for every base.slot-instance node, locates the matching base.slot-outlet in the component definition. The slot instance's content then renders in place of the outlet, preserving the declarative order defined in the schema. This origin linking ensures that runtime rendering respects the component author's intended slot structure while allowing content authors to fill slots with arbitrary compatible components.

Validation and Type Safety Guards

Beyond schema validation, Instatic implements runtime guards in src/core/visualComponents/propGuards.ts. These property guards enforce correct parameter shapes before mutations apply to the tree, catching edge cases that static typing might miss during dynamic operations.

Every boundary—UI to editor, editor to publisher, and persistence to storage—validates data against the component's TypeBox schema. When validation fails, the system surfaces toast errors in the UI immediately, preventing corrupt data from persisting or rendering.

Complete Implementation Example

The following demonstrates the full lifecycle from schema definition to slot population:

// 1️⃣ Define schema with typed params and slots
import { Type } from '@core/typebox';
import { registerVc } from '@core/visualComponents';

export const MyButtonParamsSchema = Type.Object({
  label: Type.String({ title: 'Button label' }),
  disabled: Type.Boolean({ default: false, title: 'Disabled?' })
});

export const MyButtonSlotSchema = Type.Object({
  icon: Type.Optional(Type.Any())
});

export const MyButtonSchema = Type.Intersect([
  MyButtonParamsSchema,
  Type.Object({ slots: MyButtonSlotSchema })
]);

// 2️⃣ Register the VC
registerVc('my.button', MyButtonSchema);

// 3️⃣ Create instance via API (validation automatic)
await apiRequest('/admin/api/cms/vc/create', {
  body: {
    type: 'my.button',
    params: { label: 'Buy now', disabled: false }
  },
  schema: MyButtonSchema
});

// 4️⃣ Populate slot with content
await apiRequest('/admin/api/cms/vc/slot/append', {
  body: {
    parentNodeId: '<node-id-of-my.button>',
    slotName: 'icon',
    child: { type: 'my.icon', params: { name: 'arrow' } }
  }
});

Summary

  • TypeBox schemas provide compile-time and runtime type safety for Visual Component parameters through schemas.ts and validateParams.
  • Automatic slot generation via syncSlotInstances in slotSync.ts creates base.slot-instance nodes for every declared slot.
  • Tree-agnostic instantiation through instantiateVc and mutateActiveTree allows VCs to function identically across pages, layouts, and nested components.
  • Origin tracking in origin.ts enables the publisher to resolve base.slot-outlet definitions against runtime base.slot-instance content.
  • Runtime guards in propGuards.ts enforce parameter validation at every system boundary, surfacing errors before corrupt data enters the tree.
  • Component referencing utilities in vcRefs.ts maintain the registry between component IDs and their TypeBox schemas.

Frequently Asked Questions

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

A base.slot-instance is a concrete node in the page-tree that holds user-provided content for a specific slot, created automatically when a VC is instantiated. A base.slot-outlet exists in the component definition (the origin) and marks the placeholder where slot content should render. During publishing, the system replaces outlets with their corresponding instances according to the logic in src/core/visualComponents/origin.ts.

How does Instatic ensure type safety across the editor and runtime?

Instatic validates data at every boundary using TypeBox schemas. The editor calls validateParams (which uses safeParseJson) before inserting nodes, while propGuards.ts provides runtime validation. This dual-layer approach ensures that only conforming data enters the tree and that runtime operations cannot violate the declared schema contracts.

Can Visual Components be nested within other Visual Components?

Yes. Since slot instances are standard page-tree nodes and the instantiation pipeline is tree-agnostic, you can append any compatible VC—including other VCs with their own slots—into a slot instance. The deletionImpact.ts module specifically handles propagation logic for such nested structures when components are removed from the tree.

Where does the slot content get stored in the tree structure?

Slot content becomes children of the base.slot-instance node. When you call the slot append API, the system inserts the new child node under the slot instance rather than directly under the parent VC. This hierarchical structure allows the publisher to locate content through the origin-linking mechanism in origin.ts while maintaining clean separation between component parameters and composable content.

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 →