# Visual Components in Instatic: How Slots Manage Content

> Discover Instatic's Visual Components & how slots manage content. Learn about reusable UI blocks, TypeBox schemas, and slot-instance nodes for effective content integration.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-08-01

---

**Visual Components in Instatic are server-side reusable UI building blocks defined by TypeBox schemas, while slots act as declarative outlet points synchronized via slot-instance nodes to bridge component definitions with consumer content.**

Visual Components (VCs) in the Instatic framework provide a powerful abstraction for creating reusable, server-side UI elements. This article explores how these components are structured and how the slot system manages dynamic content insertion, based on the CoreBunch/Instatic source code.

## What Are Visual Components in Instatic?

Visual Components are first-class objects that live on the server and serve as reusable UI building blocks. Each VC is defined by the `VisualComponentSchema` TypeBox schema located in [`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts), which describes three primary parts:

- **`tree`**: A flat-map node tree (`NodeTree<VCNode>`) describing the component's internal structure. These nodes use the same shape as regular page nodes (`BaseNode`) but exclude any `dynamicBindings`.
- **`params`**: An array of **VCParams** (`VCParam`) declaring configurable inputs such as strings, numbers, colors, or slots. Each parameter guarantees a stable `id`, unique `name`, and specific `VCParamType`.
- **`classIds` / `createdAt`**: Metadata fields used for styling and auditing purposes.

The schemas serve as the single source of truth, with TypeScript types derived via `Static<typeof VisualComponentSchema>`. Parsing is handled by `parseVisualComponent`, which tolerates malformed nodes by dropping them without breaking the entire component.

A VC is instantiated on a page through a **visual-component-ref** node (`base.visual-component-ref`). This reference node stores the `componentId` of the VC it embeds. During editing, the editor reads the VC's definition and constructs slot-instance children for each slot parameter, synchronizing these children with the VC's slot outlets.

## How Slots Manage Content in Instatic

Slots provide the mechanism for Visual Components to expose insertion points for consumer content. The system maintains a strict one-to-one relationship between slot declarations and slot instances through a synchronization process.

### Slot Declaration in Component Trees

Inside a VC's internal tree, slot outlets are declared using nodes with `moduleId === 'base.slot-outlet'`. The `props.slotName` property identifies the slot, defaulting to `"children"` when omitted. The `collectSlotOutletNames` function in [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts) traverses the VC tree via DFS pre-order to gather unique slot names.

```typescript
import { collectSlotOutletNames } from '@core/visualComponents/slotSync';

// vc is the parsed Visual Component
const slotNames = collectSlotOutletNames(vc.tree); 
// Returns: ['icon', 'label'] or ['children'] if default

```

### Slot-Instance Synchronization

When a VC reference appears on a page, the editor ensures the ref maintains exactly one `base.slot-instance` child per declared slot, ordered according to slot-outlet appearance in the definition. The pure function `syncSlotInstances` computes the minimal set of operations—insert, rename, or delete—required to synchronize the ref's children with the VC's current slot definitions.

```typescript
import { syncSlotInstances } from '@core/visualComponents/slotSync';

// Returns operations needed to align children with slot definitions
const result = syncSlotInstances(vcRefNode, vc, tree.nodes);

```

### Applying Slot Updates

The `applySlotSyncResult` function mutates the mutable node map within a Mutative recipe. It inserts new slot-instance nodes, renames or deletes mismatched ones, and updates the ref's `children` array to the ordered list of slot-instance IDs. This logic is implemented in [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts) between lines 66-100.

```typescript
import { applySlotSyncResult } from '@core/visualComponents/slotSync';
import { mutateActiveTree } from '@admin/pages/site/store/slices/site/helpers';

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

```

### Consumer Content Rendering

The slot-instance node exists as a normal child in the page's node tree, allowing editors to place any nodes—text, images, or even other Visual Components—underneath it. At render time, the publisher matches each `base.slot-instance` with its corresponding `base.slot-outlet` in the VC definition, stitching the consumer subtree into the VC's layout.

## Practical Implementation: Working with Visual Components and Slots

The following workflow demonstrates how to parse a stored Visual Component, identify its slots, and synchronize a page reference:

```typescript
// 1. Parse a stored Visual Component from the database
import { parseVisualComponent } from '@core/visualComponents/schemas';
import { getComponentRow } from '@core/data/componentFromRow';

const row = await getComponentRow('my-button');
const vc = parseVisualComponent(row?.data);
if (!vc) throw new Error('Invalid component');

// 2. Identify declared slot names
const slotNames = collectSlotOutletNames(vc.tree);

// 3. Synchronize the VC reference on the page
mutateActiveTree((tree) => {
  const result = syncSlotInstances(vcRefNode, vc, tree.nodes);
  applySlotSyncResult(tree.nodes, result, vcRefNode.id);
});

```

## Core Files and Architecture

The Visual Component and slot system spans several key files in the Instatic codebase:

- **[`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts)**: Defines the TypeBox schema for Visual Components, parameter types, and tolerant parsers like `parseVisualComponent`.
- **[`src/core/visualComponents/vcRefs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/vcRefs.ts)**: Centralizes detection of `base.visual-component-ref` nodes and their `componentId` extraction.
- **[`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts)**: Implements slot-outlet discovery via `collectSlotOutletNames`, synchronization logic via `syncSlotInstances`, and mutation application via `applySlotSyncResult`.
- **[`src/core/page-tree-schema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree-schema.ts)**: Provides shared `BaseNode` definitions and tree utilities used across VCs.
- **[`src/__tests__/visualComponents/vcRefs.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/visualComponents/vcRefs.test.ts)**: Contains the test suite verifying correct VC reference handling.

## Summary

- Visual Components in Instatic are server-side UI blocks defined by `VisualComponentSchema`, comprising a node tree, parameters, and metadata.
- Slots are declared via `base.slot-outlet` nodes within the VC tree, identified by `props.slotName` (defaulting to `"children"`).
- The `collectSlotOutletNames` function extracts slot identifiers from component definitions using DFS pre-order traversal.
- `syncSlotInstances` calculates minimal operations to align page references with current slot definitions, while `applySlotSyncResult` executes these mutations.
- Slot-instances act as consumer-side placeholders that hold actual content, maintaining a one-to-one relationship with slot-outlets through automated synchronization.

## Frequently Asked Questions

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

A **slot-outlet** is a declaration inside the Visual Component's internal tree (`base.slot-outlet`) that marks where content should be inserted. A **slot-instance** is a concrete node (`base.slot-instance`) created on the page as a child of the VC reference, serving as the container for consumer-provided content. The outlet defines the insertion point; the instance holds the actual content.

### How does Instatic handle malformed Visual Component data?

The `parseVisualComponent` function in [`src/core/visualComponents/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/schemas.ts) implements tolerant parsing. When encountering malformed nodes or invalid data structures, it drops the problematic nodes without throwing errors or breaking the entire component. This ensures that partially corrupted components can still render their valid portions while isolating defects.

### Can Visual Components contain other Visual Components?

Yes. Because slot-instances are standard nodes in the page tree, they can contain any valid node types, including other `base.visual-component-ref` nodes. This enables nested composition where a Visual Component can embed additional Visual Components within its slots, creating complex, hierarchical layouts while maintaining the slot synchronization guarantees at each level.

### What happens when a Visual Component's slot structure changes?

When a VC definition is modified—such as adding, removing, or reordering slots—the `syncSlotInstances` function detects these changes during the next edit session. It computes the minimal set of operations to reconcile the existing page structure with the new definition: inserting missing slot-instances, removing obsolete ones, or renaming slots to match new identifiers. The `applySlotSyncResult` function then applies these changes to maintain tree consistency.