Skip to main content

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.

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.
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; 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:
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:
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 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:
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:
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:
// 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:
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.
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.

Components and bodies

The shape of a defineComponent call and how the body executes.

Properties

Every property helper, including PropertyComponent and PropertyArray.

State and environment

How useState, useStore, and useEnvironment move data through the tree.

Schema generation

How properties and allowedComponents flow into the AI-visible tool schema.