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.

Every BindJS component is a single defineComponent call exported as the module default. The shape is small: a render body function, a properties schema describing the inputs the body receives, and a handful of optional fields — metadata, previews, thumbnail, and icon — that surface the component in editors, design tools, and component pickers. The same source produces native SwiftUI on iOS, Jetpack Compose on Android, and React on the web. You write one definition; the per-platform renderer turns the resulting AST into native views. See Native rendering for the runtime model.

A complete component

A typical component declares its properties and body as top-level constants and exports the defineComponent call. The body’s props argument is fully typed against the schema — there’s no manual ComponentProps interface to write.
const properties = {
    title: PropertyString({
        title: "Title",
        required: true,
        defaultValue: "Default Title",
    }),
    showSecondary: PropertyBoolean({
        title: "Show Secondary",
        defaultValue: true,
    }),
}

const body = (props, children) => {
    return VStack({ spacing: 20 }, [
        Text(props.title)
            .font("headline")
            .foregroundStyle(Color("primary")),

        props.showSecondary
            ? HStack({ spacing: 10 }, [
                  Button("Click Me", () => console.log("Clicked")),
                  Text("Secondary text").foregroundStyle(Color("secondary")),
              ])
            : Empty(),
    ])
}

export default defineComponent({
    metadata: {
        title: "My Component",
        description: "An example BindJS component",
    },
    properties,
    body,
})
Three things are happening here:
  1. properties declares the inputs the component accepts. Each entry uses a property helper (PropertyString, PropertyBoolean, etc.) that contributes both runtime validation and a JSON Schema fragment editors and LLMs can read. The full set of helpers is in Properties.
  2. body is the render function. It receives the typed props derived from the schema, plus a children array of any nested components passed by a parent. It returns an AST built from BindJS components and modifiers — no JSX.
  3. defineComponent is the canonical export. The runtime calls body with the resolved props on every render and walks the returned AST.
The body uses standard BindJS patterns: function calls instead of JSX, method chaining for modifiers, and conditional expressions to vary the tree based on props. Components that need state or context use hooks such as useState and useEnvironment.

The body signature

The body field takes a function of the form:
(props, children) => Component
  • props — an object whose type is inferred from the properties schema via InferProps<typeof properties> inside the defineComponent overload. You don’t declare it manually.
  • children — a Component[] array of components passed in by the caller, in the order they were supplied. Spread it into a layout (...children) or wrap it in a stack to render the contents.
  • Return value — a single Component. Use VStack, HStack, or Group to return multiple top-level views.

Inferred prop types

Given a schema, the body’s props argument has exactly the shape you’d expect:
const properties = {
    title: PropertyString({ title: "Title", required: true }),
    count: PropertyNumber({ title: "Count", defaultValue: 0 }),
    isEnabled: PropertyBoolean({ title: "Enabled", defaultValue: true }),
}

// Inferred body signature:
//   (props: { title: string; count: number; isEnabled: boolean },
//    children: Component[]) => Component
const body = (props, children) => {
    return VStack([
        Text(props.title),
        Text(`Count: ${props.count}`),
        props.isEnabled ? Text("Enabled") : Empty(),
    ])
}

export default defineComponent({ properties, body })
If you change a helper — say, swap PropertyNumber for PropertyString — the body’s props.count becomes string automatically. The schema and the body type can’t drift.
Type inference works because the helpers carry their runtime type through the type system. Declaring const properties = { ... } as const is unnecessary — and counterproductive, since it loses helper-level metadata that the inference needs.

Working with children

The children argument is a Component[] of the components a parent passed in. Two patterns cover most cases — spread the array into a layout container, or wrap it in a stack to apply common modifiers across the whole group:
const body = (props, children) =>
    VStack({ spacing: 12 }, [
        Text(props.title).font("headline"),
        ...children,
    ])
A caller supplies children as the second argument to the component’s invocation:
MyCard({ title: "Welcome" }, [
    Text("Hello, world."),
    Button("Continue", () => proceed()),
])
For components that accept slots of constrained child types — a layout that only allows Heading, Paragraph, and Image children, for example — declare those slots as named properties using PropertyComponent and PropertyArray rather than relying on the implicit children array. See Slots of child components for the patterns.

How a body differs from React

A few mechanics carry over from React, but the surface is different:
  • No JSX. Components are function calls (Text("Hi")), not elements (<Text>Hi</Text>). Modifiers chain off the result.
  • Return an AST, not React elements. The renderer walks the AST and produces native views — SwiftUI, Compose, or React DOM — depending on the platform.
  • Props are typed via the schema. No interface MyProps {...}; the helper-built properties record drives the inferred type.
  • State and context use runtime hooks. useState, useStore, and useEnvironment are runtime-injected — see State and environment.
  • Styling and behavior come from modifiers. .padding, .font, .onTapGesture, and the rest are method calls on a component, not props you pass in.

Optional fields

metadata, previews, thumbnail, and icon are optional. Components without them still render correctly; they exist to make the component discoverable, previewable, and recognizable inside editors and design tools.

metadata

Identification and discoverability information surfaced in editors, galleries, and documentation:
defineComponent({
    metadata: {
        title: "Primary Button",
        description: "A reusable filled button with a brand-colored background.",
        category: "Controls",
    },
    properties,
    body,
})
FieldTypeDescription
titlestringDisplay name shown in component pickers and inspectors.
descriptionstringOne-sentence description used in galleries and tooltips.
categorystringOptional category label used to group components.

previews

Preview instances rendered in galleries and design tools. Use Self({...}) to instantiate the component itself with sample props, then attach a .previewName(...) so each variant is labeled:
defineComponent({
    properties,
    body,
    previews: [
        Self({ title: "Preview Title", showSecondary: true })
            .previewName("Default"),
        Self({ title: "A long title that wraps", showSecondary: false })
            .previewName("Long title"),
    ],
})
Self’s prop types are inferred from the component’s own properties schema, so previews stay in sync with the inputs the body actually accepts.

thumbnail

A representative image used in component pickers. It accepts either an SVG string or a render function returning a Component:
defineComponent({
    properties,
    body,
    thumbnail: () =>
        Rectangle()
            .fill(Color("blue"))
            .frame({ width: 64, height: 32 })
            .cornerRadius(8),
})

icon

A short icon name used in menus and context menus. The host resolves the name against its icon registry:
defineComponent({
    properties,
    body,
    icon: "rectangle.fill",
})

Custom components are defineComponent calls

There’s no separate “custom component” primitive. A reusable component — a button, a card, a chart row — is defined with the same defineComponent shape as any other component. Once published, the component is callable from any other component in the same package.
// PrimaryButton.js
const properties = {
    title: PropertyString({
        title: "Button Title",
        defaultValue: "Button",
        required: true,
    }),
}

const body = (props, children) =>
    Button({
        label: Text(props.title)
            .foregroundStyle(Color("white"))
            .fontWeight("bold"),
        action: () => props.onPress?.(),
    })
        .padding(16)
        .background(Color("blue"))
        .cornerRadius(8)

export default defineComponent({
    metadata: { title: "PrimaryButton" },
    properties,
    body,
})
From any other component in the same package:
const body = (props, children) =>
    VStack({ spacing: 12 }, [
        Text("Save your work"),
        PrimaryButton({
            title: "Save Changes",
            onPress: () => console.log("Saving…"),
        }),
    ])
How user-authored components reach the runtime’s lookup is implementation-defined — see Composition and lookup for the resolution model. For Metabind specifically, components live inside immutable, semantically-versioned packages.
A component that other components call doesn’t need a metadata block, but providing one makes it discoverable in MCP App Studio and any editor that browses the package’s component list.

defineButtonStyle — the sibling primitive

BindJS exposes one other authoring primitive: defineButtonStyle, used to define a custom button style applied via the .buttonStyle() modifier. It uses the same default-export pattern as defineComponent, but with a different body signature.
export default defineButtonStyle({
    metadata: {
        title: "Capsule Button",
        description: "A pill-shaped filled button style.",
    },
    body: (configuration, props) =>
        Capsule()
            .fill(Color(props?.color || "blue"))
            .overlay(configuration.label)
            .frame({ height: 44 }),
})
The body for a button style takes a different first argument:
ArgumentTypeDescription
configuration{ label: Component, isPressed: boolean }The label the caller supplied and the current pressed state.
propsobject (optional)Style-level props. Type-inferred from properties if you declare them.
To apply a custom button style, pass it to .buttonStyle():
Button("Save", () => save())
    .buttonStyle(CapsuleButton({ color: "green" }))
Use defineButtonStyle when you want one styling treatment to wrap many call sites uniformly. For one-off buttons, apply modifiers directly to a Button(...) and skip the style primitive.

Properties

The schema-and-validation layer behind the body’s typed props.

Composition

How components find each other at runtime, recursive Self, and slots of child components.

State and environment

Component-local state with useState, shared state with useStore, and context with useEnvironment.

Quickstart

A short walkthrough that puts a component on screen end-to-end.