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 fromVCParamTypeSchema(lines 25‑34):string,number,boolean,url,enum,color,image,richText, orslot.defaultValue: The fallback used when no value is provided.required: Boolean flag forcing the editor to demand a value.enumOptions: String array forenumtypes.
// 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:
- Collects current slot names from the VC via
collectSlotOutletNames. - Matches existing
base.slot-instancechildren of the ref by name (or position for renamed slots). - Generates insert, rename, and delete operations to align the ref’s children with the VC’s current slot list.
- Returns a
SyncResultcontaining the operations, newly created nodes, and the finalorderedChildIds.
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.tsdefine typed parameters viaVCParamSchema, supporting types fromstringtorichTextwith automatic fallback handling inparseVisualComponent. - Named slots are declared implicitly by
base.slot-outletnodes in the VC tree, extracted bycollectSlotOutletNamesinslotSync.ts. - Automatic synchronization occurs through
syncSlotInstancesandapplySlotSyncResult, which materialize lockedbase.slot-instancechildren on everybase.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →