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.

Properties declare what configurable inputs a component accepts. The same schema drives four things at once: type-safe props in the body, runtime validation, form-control generation in editors that integrate BindJS, and JSON Schema generation for downstream consumers — validators, MCP tool input checks, and LLMs reasoning about how to invoke a component. You write the schema once; everything else is derived.

How properties work

The lifecycle is the same in every BindJS runtime:
  1. Definition — a component declares a properties schema in its defineComponent call. Each entry is built from a property helper such as PropertyString or PropertyEnum.
  2. Runtime resolution — at render time the runtime resolves the schema, applies defaults, and produces a typed props object that the body function receives.
  3. Editor integration — editors integrating BindJS read the schema to render input controls; updates flow back through the runtime as new prop values.
  4. Re-render — when props change, the body re-runs with the new values.
The type structure of the body’s props argument is inferred from the schema — there’s no manual ComponentProps interface to maintain. Change a helper and the body’s prop type changes with it.

Property schema structure

A component’s properties field is a record from property name to a property-helper call. The helpers compose: array helpers nest a valueType, group helpers nest a properties block, and component helpers can declare an allowedComponents allowlist for slots.
const properties = {
    title: PropertyString({
        title: "Component Title",
        description: "The main title displayed in the component",
        required: true,
        defaultValue: "Default Title",
        inspector: {
            placeholder: "Enter title...",
            showLabel: true,
            helpDescription: "This title appears at the top of the component",
        },
        validation: { minLength: 1, maxLength: 100 },
    }),

    fontSize: PropertyNumber({
        title: "Font Size",
        description: "Size of the title font in points",
        defaultValue: 16,
        validation: { min: 8, max: 72 },
        inspector: { control: "slider", step: 1, showLabel: true },
    }),

    showIcon: PropertyBoolean({
        title: "Show Icon",
        description: "Whether to display an icon next to the title",
        defaultValue: false,
    }),

    alignment: PropertyEnum({
        title: "Text Alignment",
        description: "How the text should be aligned",
        options: [
            { value: "left", label: "Left" },
            { value: "center", label: "Center" },
            { value: "right", label: "Right" },
        ],
        defaultValue: "left",
        inspector: { control: "segmented" },
    }),
}

export default defineComponent({ properties, body })
The properties field also accepts a function returning a record, for cases where the schema needs runtime context — for example, looking up enum options from the host.

Property helpers

Eleven helpers cover the input types BindJS supports: scalars, enums, dates, arrays, groups, host-resolved references (assets and content), and component slots. Every helper accepts the base fields (title, description, required, defaultValue, examples, inspector, validation) plus its own helper-specific options.

PropertyString

For text input — single-line, multi-line, code, or markdown.
PropertyString({
    title: "Text Content",
    description: "The text to display",
    required: true,
    defaultValue: "Default text",
    examples: ["Hello World", "Welcome to our app"],

    validation: {
        minLength: 1,
        maxLength: 500,
        pattern: "^[a-zA-Z0-9 ]+$",
        format: "text",
    },

    inspector: {
        placeholder: "Enter text...",
        control: "singleline",
        showLabel: true,
        helpDescription: "Enter the text you want to display",
    },
})
For multi-line text, set inspector.control: "multiline" and optionally numberOfLines. To enable a markdown formatting toolbar, set inspector.markdown: true:
PropertyString({
    title: "Description",
    description: "Detailed description text",
    inspector: {
        control: "multiline",
        numberOfLines: 5,
        markdown: true,
        placeholder: "Enter description...",
    },
    validation: { maxLength: 1000 },
})
For source code input, set inspector.control: "code".
inspector.control
"singleline" | "multiline" | "code"
The text input variant. Defaults to "singleline".
inspector.placeholder
string
Placeholder text shown when the field is empty.
inspector.markdown
boolean
Show a markdown formatting toolbar on multi-line fields.
inspector.numberOfLines
number
The visible line count for multi-line fields.
validation.minLength
number
Minimum character length.
validation.maxLength
number
Maximum character length.
validation.pattern
string
A regular expression the value must match.
validation.format
"text" | "email" | "url"
A format constraint applied alongside the pattern check.

PropertyNumber

For numeric input accepting any real number, with an input field or slider control.
PropertyNumber({
    title: "Width",
    description: "Width in pixels",
    defaultValue: 300,
    validation: { min: 0, max: 1000 },
    inspector: {
        control: "input",
        step: 10,
        placeholder: 300,
        showLabel: true,
        helpDescription: "Width must be between 0 and 1000 pixels",
    },
})
inspector.control
"input" | "slider"
The control variant. "slider" renders a draggable range; "input" renders a numeric field. Defaults to "input".
inspector.step
number
The step increment for the input or slider.
inspector.placeholder
number
A placeholder value shown when the field is empty.
validation.min
number
Minimum allowed value.
validation.max
number
Maximum allowed value.

PropertyInteger

The same shape as PropertyNumber, but restricted to integer values. The generated JSON Schema emits { type: "integer" } — useful when downstream consumers need an integer guarantee at the schema level (for example, an OpenAPI path parameter typed as integer).
PropertyInteger({
    title: "Page",
    description: "Page number (1-based)",
    defaultValue: 1,
    validation: { min: 1, max: 100 },
    inspector: { control: "input", step: 1 },
})
Use PropertyNumber when any real number is acceptable; use PropertyInteger when the consumer needs an integer guarantee at the schema level.

PropertyBoolean

For toggle and switch controls.
PropertyBoolean({
    title: "Enabled",
    description: "Whether the feature is enabled",
    defaultValue: true,
    inspector: {
        showLabel: true,
        helpDescription: "Toggle to enable or disable this feature",
    },
})
PropertyBoolean has no helper-specific options beyond the base fields.

PropertyEnum

For selection from a predefined set of options. Options can be plain strings, plain numbers, objects with value plus label (for text labels), or objects with value plus icon (for icon-only segmented controls). Plain-string options:
PropertyEnum({
    title: "Size",
    options: ["small", "medium", "large"],
    defaultValue: "medium",
    inspector: { control: "segmented" },
})
Object options with labels:
PropertyEnum({
    title: "Theme",
    description: "Visual theme for the component",
    options: [
        { value: "light", label: "Light Theme" },
        { value: "dark", label: "Dark Theme" },
        { value: "auto", label: "Auto (System)" },
    ],
    defaultValue: "auto",
    required: true,
    inspector: {
        control: "segmented",
        showLabel: true,
        helpDescription: "Choose how the component should appear",
    },
})
Icon-only segmented control:
PropertyEnum({
    title: "Alignment",
    options: [
        { value: "leading", icon: "align-left" },
        { value: "center", icon: "align-center" },
        { value: "trailing", icon: "align-right" },
    ],
    defaultValue: "leading",
    inspector: { control: "segmented" },
})
options
Array<string | number | { value, label } | { value, icon }>
required
The selectable values. Use plain strings/numbers for compact lists, { value, label } for labeled options, or { value, icon } for icon-only segmented controls.
inspector.control
"segmented" | "dropdown"
The control variant. "segmented" renders inline buttons; "dropdown" renders a select menu. Defaults to "dropdown".

PropertyDate

For date selection, with an ISO 8601 string at runtime.
PropertyDate({
    title: "Publish Date",
    description: "When to publish the content",
    defaultValue: "2024-01-01",

    validation: {
        minDate: "2024-01-01",
        maxDate: "2025-12-31",
    },

    inspector: {
        placeholder: "Select date...",
        showLabel: true,
        helpDescription: "Must be within the current year",
    },
})
inspector.placeholder
string
Placeholder text for the date picker.
validation.minDate
string
The minimum allowed date as an ISO 8601 string.
validation.maxDate
string
The maximum allowed date as an ISO 8601 string.

PropertyArray

For repeating lists of values. The item type is set by valueType — any other property helper. Array of strings:
PropertyArray({
    title: "Tags",
    description: "Tags for categorization",
    defaultValue: ["new", "featured"],

    valueType: PropertyString({
        title: "Tag",
        validation: { maxLength: 20 },
    }),

    validation: { minItems: 1, maxItems: 10 },

    inspector: {
        helpDescription: "Add up to 10 tags",
        showLabel: true,
    },
})
Array of components:
PropertyArray({
    title: "Gallery Images",
    description: "Images to display in the gallery",

    valueType: PropertyComponent({
        title: "Image",
        allowedComponents: ["StoryPhoto"],
        environment: { gallery: true },
    }),

    validation: { maxItems: 20 },
})
Array of grouped objects:
PropertyArray({
    title: "Authors",
    valueType: PropertyGroup({
        title: "Author",
        properties: {
            name: PropertyString({ title: "Name", required: true }),
            role: PropertyString({ title: "Role" }),
        },
    }),
})
valueType
PropertyHelper
required
The item type. Any property helper is allowed, including nested PropertyArray, PropertyGroup, and PropertyComponent.
validation.minItems
number
Minimum number of items.
validation.maxItems
number
Maximum number of items.

PropertyAsset

For media file selection. The host’s asset system resolves the value to an object with exactly one of image, video, audio, or model, each carrying url, dimensions, mimeType, and a host-supplied id.
PropertyAsset({
    title: "Background Image",
    description: "Image to use as background",
    assetTypes: ["image"],
    required: false,
    inspector: {
        showLabel: true,
        helpDescription: "Recommended size: 1920x1080px",
    },
})
Multiple types are allowed in the same picker:
PropertyAsset({
    title: "Media",
    assetTypes: ["image", "video"],
})
Reading an asset value inside the body:
const body = (props, children) =>
    props.asset
        ? Image({ url: props.asset.image.url })
              .resizable()
              .frame({ width: props.asset.image.dimensions.width / 2 })
        : AssetPlaceholder()
assetTypes
AssetType[]
required
The allowed asset types. Common values: "image", "video", "audio", "model". The host defines the full set.

PropertyContent

For referencing another content item. The host resolves the reference through its content system.
PropertyContent({
    title: "Related Article",
    description: "Link to a related article",
    required: false,
    inspector: {
        showLabel: true,
        helpDescription: "Select an article to link",
    },
})
PropertyContent has no helper-specific options beyond the base fields. The shape of the resolved value is host-defined.

PropertyComponent

For embedding a child component. Combine with allowedComponents to constrain the slot to a specific set of component types, and with environment to pass context values down to the child.
PropertyComponent({
    title: "Header Component",
    description: "Custom header component",
    allowedComponents: ["SiteHeader", "MinimalHeader"],

    environment: {
        theme: "dark",
        context: "header",
    },

    inspector: { showLabel: true },
})
PropertyComponent is the foundation for component slots. Wrapping it in PropertyArray produces a list slot — see Slots of child components for the patterns.
allowedComponents
string[]
Component names allowed in this slot. The host’s component registry resolves names; LLM and editor consumers use this list to narrow the candidate set.
environment
Record<string, any>
Environment values to inject into the child’s render context. Children read these via useEnvironment.

PropertyGroup

For grouping related properties under a single collapsible section. The group’s own properties field follows the same record-of-helpers structure as a component’s top-level schema.
PropertyGroup({
    title: "Author Information",
    description: "Details about the content author",

    properties: {
        name: PropertyString({
            title: "Name",
            required: true,
            validation: { maxLength: 50 },
        }),
        email: PropertyString({
            title: "Email",
            validation: { format: "email" },
        }),
        bio: PropertyString({
            title: "Biography",
            inspector: {
                control: "multiline",
                numberOfLines: 3,
            },
        }),
        avatar: PropertyAsset({
            title: "Profile Picture",
            assetTypes: ["image"],
        }),
    },

    inspector: { showLabel: true },
})
Groups nest. A group’s properties may include other groups, arrays of groups, or any other helper.
properties
Record<string, PropertyHelper>
required
The nested fields. Inferred as a nested object on the body’s props.

Slots of child components

Layout components declare where children can be placed by combining PropertyComponent (a single slot) or a PropertyArray of PropertyComponent (a list slot) with the allowedComponents allowlist. The slot’s name carries the convention.

Single section, default slot

The convention is to name the default slot components — a list of view components rendered in the layout’s main area:
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 },
    }),
}

Multi-section layout

Distinct named slots produce distinct edit regions in editors. Setting inspector.visible: false keeps the slot itself out of the inspector — the layout surfaces the children directly inside the canvas instead.
const properties = {
    sidebar: PropertyArray({
        title: "Sidebar Content",
        valueType: PropertyComponent({
            allowedComponents: ["NavList", "Promo"],
        }),
        validation: { maxItems: 5 },
        inspector: { visible: false },
    }),

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

Domain-specific slot names

For domain-specific layouts, name the slots after the things they contain. The same allowlist mechanism applies — only the named component types are accepted.
// FAQ Layout
const properties = {
    faqs: PropertyArray({
        title: "FAQ Items",
        valueType: PropertyComponent({ allowedComponents: ["FAQItem"] }),
        inspector: { visible: false },
    }),
}

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

Single-component slots

For a slot that holds exactly one child rather than a list, drop the PropertyArray wrapper:
const properties = {
    header: PropertyComponent({
        title: "Header",
        allowedComponents: ["SiteHeader", "MinimalHeader"],
    }),
}

Base fields (all property types)

Every property helper accepts the same base fields. Helper-specific options layer on top of these.
FieldTypeDescription
titlestringDisplay name in editors and the schema’s title.
descriptionstringNatural-language description used by editors, validators, and LLMs.
requiredbooleanWhether the field is required at validation time.
defaultValueTType-specific default applied when no value is supplied.
examplesT[]Example values surfaced in editors and the generated schema.
inspectorobjectEditor configuration — see Inspector configuration.
validationobjectType-specific validation rules — see Validation rules.
Treat description as the primary documentation surface. It’s what an LLM reads when reasoning about whether and how to populate the field, and what editors render below the input as help text.

Inspector configuration

The inspector object controls how the property appears in editors that render the schema as form controls. Some fields are common to every type; others are type-specific.
inspector: {
    // Common (all types)
    showLabel: true,             // Whether to show the field label
    showDivider: true,           // Whether to show a divider below the field
    helpDescription: "...",      // Tooltip / info text
    visible: (props) => true,    // Dynamic visibility — return false to hide

    // String-specific
    placeholder: "...",
    control: "singleline",        // "singleline" | "multiline" | "code"
    markdown: false,
    numberOfLines: 3,

    // Number / Integer-specific
    control: "input",             // "input" | "slider"
    step: 1,

    // Enum-specific
    control: "segmented",         // "segmented" | "dropdown"

    // Date-specific
    placeholder: "Select date...",
}

Conditional visibility

visible accepts a callback that receives the current resolved props and returns a boolean. Use it to show or hide a field based on the value of another property — for example, only showing a customColor field when colorMode === "custom":
const properties = {
    showAdvanced: PropertyBoolean({
        title: "Show Advanced Options",
        defaultValue: false,
    }),
    advancedSetting: PropertyString({
        title: "Advanced Setting",
        inspector: {
            visible: (props) => props.showAdvanced === true,
        },
    }),

    colorMode: PropertyEnum({
        title: "Color Mode",
        options: ["auto", "custom"],
        defaultValue: "auto",
    }),
    customColor: PropertyString({
        title: "Custom Color",
        inspector: {
            visible: (props) => props.colorMode === "custom",
        },
    }),
}
The callback runs whenever the form’s resolved props change, so toggling showAdvanced immediately reveals or hides advancedSetting without a re-render of the parent.

Validation rules

The validation object contains type-specific rules. Editors enforce these at input time; the runtime enforces them when resolving props before each render. String:
validation: {
    minLength: 1,
    maxLength: 500,
    pattern: "^[a-zA-Z0-9 ]+$",
    format: "text" | "email" | "url",
}
Number / Integer:
validation: {
    min: 0,
    max: 100,
}
Array:
validation: {
    minItems: 1,
    maxItems: 20,
}
Date:
validation: {
    minDate: "2024-01-01",
    maxDate: "2025-12-31",
}
PropertyBoolean, PropertyEnum, PropertyAsset, PropertyContent, PropertyComponent, and PropertyGroup don’t add helper-specific validation rules beyond the base required field. Group validation flows through the nested helpers.

Property type reference

The full set of helpers, with their runtime and schema mappings:
HelperRuntime typeJSON Schema typeNotes
PropertyStringstringstringText input — singleline, multiline, or code controls.
PropertyNumbernumbernumberReal numbers; input or slider control.
PropertyIntegerintegerintegerInteger-only; same shape as PropertyNumber.
PropertyBooleanbooleanbooleanToggle / switch.
PropertyEnumenumstring/number enumsegmented or dropdown control.
PropertyDatedatestring (ISO 8601)Date picker.
PropertyArrayarrayarrayRepeatable list, item type set by valueType.
PropertyGroupgroupnested objectNested fields under one collapsible section.
PropertyAssetassethost-definedImage / video / audio / 3D model picker; resolved by the host’s asset system.
PropertyContentcontenthost-definedReference to another content item; resolved by the host’s content system.
PropertyComponentcomponentrecursiveEmbedded child component, with allowedComponents allowlist for recursion.

Schema generation for editors and LLMs

Every property helper has a deterministic mapping to JSON Schema. A consumer — an inspector form, a validator, an LLM — can read a component’s properties schema and know exactly what a valid props object for that component looks like, without having to execute the body. This is the contract that makes BindJS usable as a target for editor-driven and LLM-generated UI.

What flows into the generated schema

  • Type structure — each helper emits a corresponding JSON Schema fragment ({ type: "string" }, { type: "integer" }, { type: "array", items: ... }). Nested helpers — PropertyArray({ valueType: PropertyGroup({...}) }), for example — compose into nested schema.
  • title — included as the schema’s title. Used as the field label in editors and as a human-readable identifier for LLM consumers.
  • description — included as the schema’s description. This is the primary documentation surface for the field — what an LLM sees when reasoning about whether and how to populate it.
  • required / defaultValue — included as the schema’s required array entries and default values.
  • validationminLength, maxLength, min, max, pattern, minItems, maxItems, and uniqueItems flow through as the equivalent JSON Schema constraints.
  • examples — included as the schema’s examples array, which both editors and LLMs use as concrete guidance.
  • inspector hints — host-specific (control type, placeholder, help text). Editors that adopt them get richer forms; consumers that don’t can ignore them.

Recursive component schemas

PropertyComponent declares a slot for a nested component, optionally constrained to a specific set of component types via allowedComponents:
PropertyComponent({
    title: "Hero",
    allowedComponents: ["StoryPhoto", "StoryVideo", "StoryParagraph"],
})
A schema generator walks this recursively — looking up each name in allowedComponents, generating a schema for that component’s own properties, and emitting the full set as a oneOf (or equivalent) in the parent’s schema. Walk repeatedly and you get a schema for an entire component subtree, rooted at any starting component. The same applies to PropertyArray({ valueType: PropertyComponent({ allowedComponents: [...] }) }) — the array’s items schema is the recursive expansion.

Why this matters

The combination — typed properties, description as natural-language documentation, and allowedComponents as a navigable component graph — means a single root component carries enough information for an LLM to generate a valid invocation of the whole subtree, or for a host to validate an LLM’s output against the schema before rendering. Three things follow:
  • Editors get inspectors for free. Any tool that can render a JSON Schema form can render an inspector for any BindJS component, without per-component handwritten UI.
  • LLMs see structured documentation. The description field on every property is the surface the LLM reads when reasoning about how to populate the schema. Treat it as the doc string, not as a label — every property the LLM is supposed to fill should describe its intent in prose.
  • Hosts can validate before rendering. An LLM’s response can be checked against the generated schema before it’s passed to a renderer. Invalid output is caught at the edge, not by the renderer crashing on a malformed prop.

How Metabind uses it

Metabind realizes this contract in two places. Content types in MCP App Studio. When a root BindJS component is bound to a content type in MCP App Studio, the CMS recursively walks the component’s properties, expanding each PropertyComponent({ allowedComponents }) into the schemas of its allowed children, and so on. The result is a single JSON Schema that describes a valid content document for the whole subtree. The CMS uses it to render inspector forms, validate edits, and version-lock the schema with the published package — when you publish a new version of a component, content created against the old schema continues to validate against the version it was authored under. Interactive Tools in the Cloud MCP App Server. An Interactive Tool declares an allowlist of components the LLM is permitted to render. The Cloud MCP App Server uses the same recursive walker over those components’ properties to produce the tool’s input schema, which is published as the tool’s inputSchema in MCP. The LLM sees the schema — including every description field — and generates a structured response. The server validates the response against the schema before the props ever reach the renderer. In both cases, the only thing the consumer needs from your code is the properties shape. There’s no separate schema file to maintain — your component’s typed inputs and the schema editors and LLMs see are the same artifact.

Components and bodies

How properties fits inside defineComponent alongside body, metadata, and previews.

Composition

Slots of child components, recursive Self, and how component names resolve at runtime.

Talking to the MCP host

How a BindJS view calls back into the chat — tool calls, messaging, display modes.

Layout — Stacks

VStack, HStack, ZStack — the layout primitives a component body returns.