> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Composition and slots

> How component identifiers are resolved at runtime, how components compose, and how to declare slots for child components

Components are composed by calling them inside another component's body. When you write `Text(...)`, `VStack(...)`, or `MyCustomCard(...)`, the runtime looks the name up in a name-to-implementation map and returns a builder that participates in the AST. That same lookup is what lets a layout component reference view components it allows, lets a component recurse into itself, and lets the host swap an implementation for a custom one.

This page explains how the lookup works, the patterns you use to compose components, and how to declare slots that accept child components.

## Calling other components

The most common form of composition is direct usage: a component's body calls other components by name and arranges them with stacks and modifiers.

```typescript theme={null}
const body = (props, children) =>
    StoryLayout({ title: props.title, subtitle: props.subtitle }, [
        StoryParagraph({ text: props.introText }),
        StoryPhoto({ asset: props.headerImage }),
    ])
```

Three things are required for a name to resolve at runtime:

1. **Built-in components are always available.** Every name in the BindJS type definitions — `VStack`, `HStack`, `Text`, `Image`, `Button`, and the rest — is registered before any component code runs.
2. **The host registers additional names.** Custom components are added to the lookup before the body executes. In Metabind, this happens automatically through the published [package](/guides/concepts/components); the components in your project are registered against the active package set when the runtime starts.
3. **Unknown names render as a placeholder.** Calling an unregistered name does not crash the runtime. It renders a placeholder and surfaces a diagnostic to the host, so a missing dependency is visible without taking the surrounding tree down.

You compose any registered component by calling it. The shape is the same whether the target is built-in or custom.

## Self-reference

Use `Self({...})` inside a component's body to render the component recursively with different props. `Self`'s prop types are inferred from the component's own `properties` schema, so the call is type-checked against the same shape the body already receives.

A recursive list is the canonical case — items at the top level can have nested children, and the same component renders both levels:

```typescript theme={null}
const properties = {
    title: PropertyString({ title: "Title", required: true }),
    items: PropertyArray({
        title: "Items",
        valueType: PropertyGroup({
            properties: {
                title: PropertyString({ title: "Item title" }),
                children: PropertyArray({
                    title: "Children",
                    valueType: PropertyString({ title: "Child" }),
                }),
            },
        }),
    }),
}

const body = (props, children) =>
    VStack([
        Text(props.title),
        ...(props.items || []).map((item) =>
            item.children
                ? Self({ title: item.title, items: item.children })
                : Text(item.title)
        ),
    ])

export default defineComponent({ properties, body })
```

`Self` only references the current component. To recurse through a different component, call that component by name.

## Environment-based conditional rendering

A component can adapt its output to the surrounding environment. Read the environment with `useEnvironment()` and branch on values that ancestor components or the host has set:

```typescript theme={null}
const body = (props, children) => {
    const env = useEnvironment()

    if (env.gallery) {
        return props.asset ? Image(props.asset.image) : AssetPlaceholder()
    }

    return VStack({ spacing: 0 }, [
        props.asset ? Image(props.asset.image) : AssetPlaceholder(),
        props.caption ? Text(props.caption) : Empty(),
    ])
}
```

The same component renders a stripped-down image in a gallery context and the full caption layout everywhere else. The environment is set by an ancestor with `.environment(key, value)` and is visible only inside that ancestor's subtree — see [State and environment](/bindjs/authoring/state) for how values flow and the keys a runtime injects.

## Slots of child components

A layout component declares where children can be placed by combining `PropertyComponent` (a single slot) or `PropertyArray` of `PropertyComponent` (a list slot) with the `allowedComponents` option. The names you choose for the slots become the prop names the body reads — the schema, the editor inspector, and the AI tool call all use the same vocabulary.

There are three patterns. Pick the one that matches the layout's structure.

### Single section

When the layout has one content area, use `components` as the slot name. This is the convention editors expect for the default content slot:

```typescript theme={null}
const properties = {
    title: PropertyString({ title: "Page Title", required: true }),

    components: PropertyArray({
        title: "Page Content",
        description: "Main content components",
        valueType: PropertyComponent({
            allowedComponents: ["Heading", "Paragraph", "Image", "Quote"],
        }),
        inspector: { visible: false },
    }),
}
```

`inspector: { visible: false }` hides the slot from the form-style inspector — the slot is configured by dropping components into the visual canvas, not by editing a list field.

### Multiple sections

When the layout has more than one content area, declare each as its own slot with its own `allowedComponents` list and, where appropriate, validation rules:

```typescript theme={null}
const properties = {
    sidebar: PropertyArray({
        title: "Sidebar Content",
        description: "Components for the sidebar",
        valueType: PropertyComponent({
            allowedComponents: ["NavList", "Promo"],
        }),
        validation: { maxItems: 5 },
        inspector: { visible: false },
    }),

    main: PropertyArray({
        title: "Main Content",
        description: "Primary content area",
        valueType: PropertyComponent({
            allowedComponents: ["Heading", "Paragraph", "Image"],
        }),
        validation: { minItems: 1 },
        inspector: { visible: false },
    }),
}
```

`maxItems` and `minItems` apply to the list at the schema level, so an LLM that generates input against the tool's schema sees the same constraints the editor enforces.

### Domain-specific naming

When the slot represents a specific kind of content, name it for what it holds. The slot name shows up in the inspector and in the schema the AI sees, so a domain name reads better than a generic one:

```typescript theme={null}
// FAQ Layout
const properties = {
    faqs: PropertyArray({
        title: "FAQ Items",
        description: "Frequently asked questions",
        valueType: PropertyComponent({ allowedComponents: ["FAQItem"] }),
        inspector: { visible: false },
    }),
}

// Recipe Layout
const properties = {
    ingredients: PropertyArray({
        title: "Ingredients",
        description: "Recipe ingredients",
        valueType: PropertyComponent({ allowedComponents: ["Ingredient"] }),
        inspector: { visible: false },
    }),
    steps: PropertyArray({
        title: "Instructions",
        description: "Step-by-step instructions",
        valueType: PropertyComponent({ allowedComponents: ["Step"] }),
        inspector: { visible: false },
    }),
}
```

A typed slot like `ingredients: PropertyArray({ valueType: PropertyComponent({ allowedComponents: ["Ingredient"] }) })` produces a much sharper schema than a generic `components` slot would — the AI sees exactly what kind of child belongs there.

### Single-component slots

For a slot that holds one component instead of a list, drop the `PropertyArray` wrapper and use `PropertyComponent` directly:

```typescript theme={null}
const properties = {
    header: PropertyComponent({
        title: "Header",
        allowedComponents: ["SiteHeader", "MinimalHeader"],
    }),
}
```

The body reads `props.header` as a single component. Use this for chrome that is structurally singular — a header, a hero, a CTA — rather than for repeating content.

<Note>
  `allowedComponents` is the boundary the runtime governs. A slot that lists `["FAQItem"]` will not accept anything else, even if the AI returns a different component shape. The same allowlist drives schema generation, so what the LLM is allowed to choose matches what the renderer is allowed to render.
</Note>

## What to read next

<CardGroup cols={2}>
  <Card title="Components and bodies" icon="cube" href="/bindjs/authoring/components">
    The shape of a `defineComponent` call and how the body executes.
  </Card>

  <Card title="Properties" icon="sliders" href="/bindjs/authoring/properties">
    Every property helper, including `PropertyComponent` and `PropertyArray`.
  </Card>

  <Card title="State and environment" icon="circle-nodes" href="/bindjs/authoring/state">
    How `useState`, `useStore`, and `useEnvironment` move data through the tree.
  </Card>

  <Card title="Schema generation" icon="file-code" href="/bindjs/authoring/properties#schema-generation-for-editors-and-llms">
    How `properties` and `allowedComponents` flow into the AI-visible tool schema.
  </Card>
</CardGroup>
