> ## 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.

# Quickstart

> Build, configure, and preview your first BindJS component in five steps

In this tutorial you'll build a small BindJS component from scratch — a configurable call-to-action card with a title, a color variant, a counter that increments on tap, and previews for the component gallery. By the end you'll have touched every piece of a working component: metadata, properties, a body that returns a layout, state via `useState`, and `Self` previews.

<Note>
  The component you build here renders as React on the web, SwiftUI on iOS, and Jetpack Compose on Android from this single source. You don't write a separate version per platform.
</Note>

## Prerequisites

Before you start, you need:

* A Metabind project with access to MCP App Studio. If you haven't created one yet, start with [Your first MCP App](/guides/getting-started/your-first-mcp-app), then come back here.
* Familiarity with TypeScript or modern JavaScript. You don't need React or SwiftUI experience.

You'll author the component in MCP App Studio's component editor. No local toolchain is required.

## Build the component

<Steps>
  <Step title="Define metadata">
    Every component begins with metadata that describes how it's surfaced in the studio's component picker, in the gallery, and in any preview UI. The fields are short and human-readable.

    ```typescript theme={null}
    const metadata = {
      title: "Action card",
      description: "A configurable card with a title, a color variant, and a tap counter",
      category: "Cards",
    }
    ```

    `title` is the name shown in the picker. `description` helps teammates understand what the component does. `category` groups the component alongside others — pick a name that fits your project's organization (for example, `"Cards"`, `"Controls"`, `"Layout"`).
  </Step>

  <Step title="Define properties">
    Properties make a component configurable. Each property is declared with a helper function — `PropertyString`, `PropertyEnum`, `PropertyNumber`, `PropertyBoolean`, and so on — and the schema you build does three things at once: it generates typed `props` for the body, it validates inputs at runtime, and it tells the studio which inspector control to render.

    For the action card, you need a title and a color variant.

    ```typescript theme={null}
    const properties = {
      title: PropertyString({
        title: "Title",
        description: "The card's main label",
        required: true,
        defaultValue: "Get started",
      }),

      variant: PropertyEnum({
        title: "Variant",
        description: "Color treatment for the card",
        options: [
          { value: "blue", label: "Blue" },
          { value: "green", label: "Green" },
          { value: "red", label: "Red" },
        ],
        defaultValue: "blue",
        inspector: { control: "segmented" },
      }),
    }
    ```

    A few things to notice. `required: true` on `title` means the component won't render without one. `defaultValue` makes the property safe to omit. `options` on `PropertyEnum` is a list of `{ value, label }` pairs — the value is what your `body` receives; the label is what the inspector shows. The `inspector.control: "segmented"` hint asks the studio to render a segmented control instead of a dropdown.

    <Tip>
      The body's `props` argument is typed automatically from this schema. `props.title` is `string`, `props.variant` is `"blue" | "green" | "red"`. You don't need to write a separate interface.
    </Tip>
  </Step>

  <Step title="Write the body function">
    The body is a function that receives `props` and `children` and returns a component tree. You compose the tree from BindJS primitives (`VStack`, `HStack`, `Text`, `Button`, shapes) and apply modifiers like `.padding()`, `.background()`, and `.cornerRadius()` by chaining methods.

    Start with a simple layout — a vertical stack containing the title, on a colored background.

    ```typescript theme={null}
    const body = (props, children) =>
      VStack({ spacing: 12, alignment: "leading" }, [
        Text(props.title)
          .font("title2")
          .fontWeight("semibold")
          .foregroundStyle(Color("white")),

        Text("Tap the button below").foregroundStyle(Color("white").opacity(0.85)),
      ])
        .padding(20)
        .frame({ maxWidth: ".infinity", alignment: "leading" })
        .background(Color(props.variant))
        .cornerRadius(16)
    ```

    The flow reads top to bottom: build a `VStack` of two `Text` views, then apply padding, an expanded frame, a background color tied to the variant, and a corner radius — all by chaining modifiers off the stack itself.
  </Step>

  <Step title="Add interactivity with useState">
    Hooks let a component hold local state across renders. `useState` is the basic one, mirroring its React counterpart: it returns a value and a setter, and changing the value re-runs the body.

    Add a tap counter and a button that increments it.

    ```typescript theme={null}
    const body = (props, children) => {
      const [taps, setTaps] = useState(0)

      return VStack({ spacing: 12, alignment: "leading" }, [
        Text(props.title)
          .font("title2")
          .fontWeight("semibold")
          .foregroundStyle(Color("white")),

        Text(`Tapped ${taps} ${taps === 1 ? "time" : "times"}`)
          .foregroundStyle(Color("white").opacity(0.85)),

        Button("Tap me", () => setTaps(taps + 1))
          .padding({ horizontal: 16, vertical: 8 })
          .background(Color("white").opacity(0.2))
          .foregroundStyle(Color("white"))
          .cornerRadius(8),
      ])
        .padding(20)
        .frame({ maxWidth: ".infinity", alignment: "leading" })
        .background(Color(props.variant))
        .cornerRadius(16)
    }
    ```

    `Button` takes a label and an action. When you call `setTaps(taps + 1)`, the runtime re-runs the body with the new value, the third `Text` re-renders, and the AST diff is sent to the renderer.

    <Note>
      Hooks can only be called inside a body function. If you ever see "hooks used outside a component" in development, check that you're not calling `useState` at the module top level.
    </Note>
  </Step>

  <Step title="Add previews">
    Previews show how the component looks under different inputs. They appear in the component gallery, in design reviews, and anywhere the studio needs to render the component without real data. Use the `Self` helper to instantiate the current component with sample props, and `.previewName()` to label each preview.

    ```typescript theme={null}
    const previews = [
      Self({ title: "Get started", variant: "blue" }).previewName("Default"),
      Self({ title: "Confirmed", variant: "green" }).previewName("Success"),
      Self({ title: "Action required", variant: "red" }).previewName("Warning"),
    ]
    ```

    Aim for previews that cover the meaningful states of the component — variants, edge-case content (long titles, empty strings), and any combination a teammate might want to inspect at a glance.
  </Step>
</Steps>

## Complete code

Here's the full component, ready to paste into MCP App Studio.

```typescript theme={null}
const metadata = {
  title: "Action card",
  description: "A configurable card with a title, a color variant, and a tap counter",
  category: "Cards",
}

const properties = {
  title: PropertyString({
    title: "Title",
    description: "The card's main label",
    required: true,
    defaultValue: "Get started",
  }),

  variant: PropertyEnum({
    title: "Variant",
    description: "Color treatment for the card",
    options: [
      { value: "blue", label: "Blue" },
      { value: "green", label: "Green" },
      { value: "red", label: "Red" },
    ],
    defaultValue: "blue",
    inspector: { control: "segmented" },
  }),
}

const body = (props, children) => {
  const [taps, setTaps] = useState(0)

  return VStack({ spacing: 12, alignment: "leading" }, [
    Text(props.title)
      .font("title2")
      .fontWeight("semibold")
      .foregroundStyle(Color("white")),

    Text(`Tapped ${taps} ${taps === 1 ? "time" : "times"}`)
      .foregroundStyle(Color("white").opacity(0.85)),

    Button("Tap me", () => setTaps(taps + 1))
      .padding({ horizontal: 16, vertical: 8 })
      .background(Color("white").opacity(0.2))
      .foregroundStyle(Color("white"))
      .cornerRadius(8),
  ])
    .padding(20)
    .frame({ maxWidth: ".infinity", alignment: "leading" })
    .background(Color(props.variant))
    .cornerRadius(16)
}

const previews = [
  Self({ title: "Get started", variant: "blue" }).previewName("Default"),
  Self({ title: "Confirmed", variant: "green" }).previewName("Success"),
  Self({ title: "Action required", variant: "red" }).previewName("Warning"),
]

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

Save the file in MCP App Studio. The studio compiles the component, runs the body against each preview, and shows the results in the gallery. Edit any field and the preview updates.

## Using the component elsewhere

Once a component is published, any other component in the same package can call it by name. The studio infers the function signature from the `properties` schema, so editor autocomplete shows you the available props.

```typescript theme={null}
ActionCard({ title: "Welcome back", variant: "green" })
```

You can also chain modifiers onto a custom component, the same way you would on a built-in.

```typescript theme={null}
ActionCard({ title: "Heads up", variant: "red" })
  .padding(8)
  .shadow({ radius: 6, y: 2 })
```

## What to do next

<CardGroup cols={2}>
  <Card title="Authoring components" icon="code" href="/bindjs/authoring/components">
    Metadata, body, previews, and the full `defineComponent` shape.
  </Card>

  <Card title="Properties" icon="sliders" href="/bindjs/authoring/properties">
    Every property helper, validation, and inspector control.
  </Card>

  <Card title="MCP host integration" icon="grid" href="/bindjs/authoring/mcp-host">
    Turning a component into an Interactive Tool an AI host can call.
  </Card>

  <Card title="State and hooks" icon="rotate" href="/bindjs/authoring/state">
    `useState`, `useStore`, and the runtime hook surface.
  </Card>
</CardGroup>
