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

# Controls

> Buttons, toggles, pickers, menus, labels, and progress indicators

export const PlatformStatuses = ({statuses}) => {
  const StatusBadge = ({status, label}) => {
    const styles = {
      green: {
        backgroundColor: '#dcfce7',
        color: '#166534'
      },
      orange: {
        backgroundColor: '#fed7aa',
        color: '#9a3412'
      },
      red: {
        backgroundColor: '#fecaca',
        color: '#991b1b'
      },
      gray: {
        backgroundColor: '#f3f4f6',
        color: '#4b5563'
      }
    };
    const baseStyle = {
      display: 'inline-flex',
      alignItems: 'center',
      padding: '0.125rem 0.625rem',
      borderRadius: '9999px',
      fontSize: '0.875rem',
      fontWeight: '500'
    };
    const colorStyle = styles[status] || styles.green;
    return <span style={{
      ...baseStyle,
      ...colorStyle
    }}>
        {label || status}
      </span>;
  };
  const STATUS_CONFIG = {
    supported: {
      label: "Supported",
      color: "green"
    },
    partial: {
      label: "Partial",
      color: "orange"
    },
    "not-implemented": {
      label: "Not Implemented",
      color: "gray"
    }
  };
  const renderCard = (platform, value) => {
    if (!value) return null;
    const {status, note} = typeof value === "string" ? {
      status: value
    } : value;
    const config = STATUS_CONFIG[status];
    if (!config) return null;
    const titleMap = {
      ios: "SwiftUI",
      android: "Jetpack Compose",
      web: "Web"
    };
    return <Card key={platform} title={titleMap[platform] || platform}>
          <StatusBadge status={config.color} label={config.label} />
          {note && <div style={{
      marginTop: '0.5rem',
      fontSize: '0.875rem',
      color: '#6b7280'
    }}>
              {note}
            </div>}
      </Card>;
  };
  if (statuses == null) {
    return null;
  }
  return <Columns cols="3">
      {Object.entries(statuses).map(([platform, value]) => renderCard(platform, value))}
    </Columns>;
};

export const ComposeJS = ({code, name, height}) => {
  const encodedCode = useMemo(() => {
    if (!code) return "";
    try {
      return btoa(code);
    } catch (e) {
      console.error("Failed to encode code", e);
      return "";
    }
  }, [code]);
  if (!encodedCode) {
    return null;
  }
  return <iframe src={`https://www.metabind.ai/embed?code=${encodedCode}&name=${name ?? 'Example'}`} loading="lazy" style={{
    width: "100%",
    height: height || '350px',
    border: "1px solid #e5e7eb",
    borderRadius: "var(--rounded-2xl,1rem)",
    overflow: "hidden"
  }} title="ComposeJS Preview" />;
};

The interactive control primitives. `Button` performs an action on tap. `Toggle` flips a boolean. `Picker` selects one value from a set, and `Menu` shows a dropdown list of actions. `Label` is a title-plus-icon combination commonly used as a button or row label, and `ProgressView` displays determinate or indeterminate progress.

## Button

A tappable control that performs an action when pressed.

```typescript theme={null}
Button(props: { action: () => void; label: Component }): Component;
Button(label: string, action: () => void): Component;
Button(label: Component, action: () => void): Component;
```

<ParamField path="label" type="string | Component" required>
  The button's visible content. Can be a plain string or any component for custom layouts.
</ParamField>

<ParamField path="action" type="() => void" required>
  Callback invoked when the button is tapped.
</ParamField>

When using the object form:

<ParamField path="props.action" type="() => void" required>
  Callback invoked when the button is tapped.
</ParamField>

<ParamField path="props.label" type="Component" required>
  A component to use as the button label.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "supported" },
web: { status: "supported" },
}}
/>

**Simple text button**

```typescript theme={null}
Button("Save", () => {
    console.log("Saved!")
})
```

**Button with component label**

```typescript theme={null}
Button(
    HStack({ spacing: 8 }, [
        Image({ systemName: "checkmark" }),
        Text("Save")
    ]),
    () => handleSave()
)
```

**Object form**

```typescript theme={null}
Button({
    action: () => handleSave(),
    label: HStack([
        Image({ systemName: "checkmark" }),
        Text("Save")
    ])
})
```

**Styled button**

```typescript theme={null}
Button("Primary Action", () => {})
    .foregroundStyle(Color("white"))
    .padding({ horizontal: 16, vertical: 10 })
    .background(Color("blue"))
    .cornerRadius(8)
```

**Disabled button**

```typescript theme={null}
Button("Submit", () => {})
    .disabled(true)
```

## Toggle

A switch control for toggling a boolean value on or off.

```typescript theme={null}
Toggle(props: {
    label?: string;
    isOn: boolean;
    setIsOn: (value: boolean) => void;
}): Component;
```

<ParamField path="label" type="string" optional>
  A text label displayed alongside the toggle switch.
</ParamField>

<ParamField path="isOn" type="boolean" required>
  The current state of the toggle.
</ParamField>

<ParamField path="setIsOn" type="(value: boolean) => void" required>
  Callback invoked when the user taps the toggle. Use this to update your state.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "partial", note: "label prop is not implemented; renders as Switch only" },
web: { status: "supported" },
}}
/>

**Basic toggle**

```typescript theme={null}
const body = () => {
    const [enabled, setEnabled] = useState(false)
    return Toggle({ label: "Notifications", isOn: enabled, setIsOn: setEnabled })
}
```

**With tint color**

```typescript theme={null}
const body = () => {
    const [darkMode, setDarkMode] = useState(false)
    return Toggle({ label: "Dark Mode", isOn: darkMode, setIsOn: setDarkMode })
        .tint(Color("green"))
}
```

**Settings list**

```typescript theme={null}
const body = () => {
    const [wifi, setWifi] = useState(true)
    const [bluetooth, setBluetooth] = useState(false)
    return VStack({ spacing: 0 }, [
        Toggle({ label: "Wi-Fi", isOn: wifi, setIsOn: setWifi })
            .padding(16),
        Divider(),
        Toggle({ label: "Bluetooth", isOn: bluetooth, setIsOn: setBluetooth })
            .padding(16),
    ])
}
```

## Slider

A continuous control for selecting a numeric value within a range.

```typescript theme={null}
Slider(props: {
    value: number;
    setValue: (value: number) => void;
    range?: [number, number];
    lowerBound?: number;
    upperBound?: number;
    step?: number;
    label?: string;
    minimumValueLabel?: Component;
    maximumValueLabel?: Component;
}): Component;
```

<ParamField path="value" type="number" required>
  The current value of the slider.
</ParamField>

<ParamField path="setValue" type="(value: number) => void" required>
  Callback invoked as the user drags the slider. Use this to update your state.
</ParamField>

<ParamField path="range" type="[number, number]" optional>
  The slider's range as `[lowerBound, upperBound]`. The convenient form for setting both bounds at once.
</ParamField>

<ParamField path="lowerBound" type="number" optional>
  The minimum value. Use instead of `range` when setting only one bound.
</ParamField>

<ParamField path="upperBound" type="number" optional>
  The maximum value. Use instead of `range` when setting only one bound.
</ParamField>

<ParamField path="step" type="number" optional>
  The increment between selectable values. When omitted, the slider is continuous.
</ParamField>

<ParamField path="label" type="string" optional>
  A text label describing what the slider controls. Some hosts display the label alongside the slider.
</ParamField>

<ParamField path="minimumValueLabel" type="Component" optional>
  A component shown at the lower end of the slider, typically a small `Text`.
</ParamField>

<ParamField path="maximumValueLabel" type="Component" optional>
  A component shown at the upper end of the slider.
</ParamField>

<PlatformStatuses
  statuses={{
ios: "not-implemented",
android: "not-implemented",
web: { status: "supported" },
}}
/>

**Basic slider**

```typescript theme={null}
const body = () => {
    const [volume, setVolume] = useState(50)
    return Slider({
        value: volume,
        setValue: setVolume,
        range: [0, 100],
    })
}
```

**Stepped slider with labels**

```typescript theme={null}
const body = () => {
    const [volume, setVolume] = useState(50)
    return Slider({
        value: volume,
        setValue: setVolume,
        range: [0, 100],
        step: 5,
        label: "Volume",
        minimumValueLabel: Text("0").font("caption"),
        maximumValueLabel: Text("100").font("caption"),
    })
}
```

**Bounded with lowerBound and upperBound**

```typescript theme={null}
const body = () => {
    const [opacity, setOpacity] = useState(1)
    return Slider({
        value: opacity,
        setValue: setOpacity,
        lowerBound: 0,
        upperBound: 1,
        step: 0.1,
    })
}
```

## Picker

A selection control for choosing from a set of mutually exclusive values.

```typescript theme={null}
Picker<V extends Hashable = string>(
    label: string,
    selection: Binding<V>,
    children: Component[]
): Component;
```

<ParamField path="label" type="string" required>
  The label text for the picker.
</ParamField>

<ParamField path="selection" type="Binding<V>" required>
  A binding to the selected value, provided as a `[value, setValue]` tuple. The value type must be `string` or `number`.
</ParamField>

<ParamField path="children" type="Component[]" required>
  An array of picker options. Each child should have a `.tag()` modifier to identify its value.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "supported" },
web: { status: "supported" },
}}
/>

**Basic picker**

```typescript theme={null}
const body = () => {
    const [color, setColor] = useState("red")
    return Picker("Color", [color, setColor], [
        Text("Red").tag("red"),
        Text("Green").tag("green"),
        Text("Blue").tag("blue")
    ])
}
```

**Segmented picker**

```typescript theme={null}
const body = () => {
    const [size, setSize] = useState("m")
    return Picker("Size", [size, setSize], [
        Text("S").tag("s"),
        Text("M").tag("m"),
        Text("L").tag("l"),
        Text("XL").tag("xl")
    ])
        .pickerStyle("segmented")
}
```

**Picker in a form**

```typescript theme={null}
const body = () => {
    const [country, setCountry] = useState("us")
    const [language, setLanguage] = useState("en")
    return VStack({ spacing: 16 }, [
        Picker("Country", [country, setCountry], [
            Text("United States").tag("us"),
            Text("Canada").tag("ca"),
            Text("United Kingdom").tag("uk")
        ]),
        Picker("Language", [language, setLanguage], [
            Text("English").tag("en"),
            Text("French").tag("fr"),
            Text("Spanish").tag("es")
        ])
    ])
}
```

**With ForEach**

```typescript theme={null}
const body = () => {
    const options = ["Option 1", "Option 2", "Option 3"]
    const [selected, setSelected] = useState(options[0])
    return Picker("Choose", [selected, setSelected], [
        ForEach(options, (option) =>
            Text(option).tag(option)
        )
    ])
}
```

<Note>
  Each child must have a `.tag()` value matching the type of the selection binding. Style the picker with `.pickerStyle()` using values like `"menu"`, `"segmented"`, `"wheel"`, or `"inline"`. See [PickerStyle](/bindjs/types/PickerStyle).
</Note>

## Menu

A dropdown menu of actions that appears when the label is tapped.

```typescript theme={null}
Menu(props: { label: Component }, children: Component[]): Component;
```

<ParamField path="props.label" type="Component" required>
  The component displayed as the menu trigger. Typically a Button, Text, or Label.
</ParamField>

<ParamField path="children" type="Component[]" required>
  An array of menu items. Typically Button components, with optional Divider separators and nested Menu components for submenus.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "supported" },
web: "not-implemented",
}}
/>

**Simple menu**

```typescript theme={null}
Menu({ label: Text("Options") }, [
    Button("Edit", () => handleEdit()),
    Button("Delete", () => handleDelete())
])
```

**Menu with icon label**

```typescript theme={null}
Menu({ label: Image({ systemName: "ellipsis.circle" }) }, [
    Button("Share", () => {}),
    Button("Copy Link", () => {}),
    Divider(),
    Button("Report", () => {})
])
```

**Nested submenu**

```typescript theme={null}
Menu({ label: Text("Format") }, [
    Button("Bold", () => toggleBold()),
    Button("Italic", () => toggleItalic()),
    Divider(),
    Menu({ label: Text("Text Size") }, [
        Button("Small", () => setSize("small")),
        Button("Medium", () => setSize("medium")),
        Button("Large", () => setSize("large"))
    ])
])
```

**Menu with button items using icons**

```typescript theme={null}
Menu({ label: Image({ systemName: "gear" }) }, [
    Button({
        label: Label({ title: "Profile", systemImage: "person" }),
        action: () => showProfile()
    }),
    Button({
        label: Label({ title: "Notifications", systemImage: "bell" }),
        action: () => showNotifications()
    }),
    Divider(),
    Button({
        label: Label({ title: "Sign Out", systemImage: "arrow.right.square" }),
        action: () => signOut()
    })
])
```

## Label

A standard label combining a title with an optional icon.

```typescript theme={null}
Label(props: {
    title: string | Component;
    icon?: Component;
    systemImage?: string;
}): Component;
```

<ParamField path="title" type="string | Component" required>
  The label text. Can be a plain string or a component for custom styling.
</ParamField>

<ParamField path="icon" type="Component" optional>
  A custom icon component displayed alongside the title.
</ParamField>

<ParamField path="systemImage" type="string" optional>
  A system symbol name (SF Symbols) to use as the icon. Mutually exclusive with `icon`.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "partial", note: "icon prop not supported; only systemImage and title work" },
web: "not-implemented",
}}
/>

**Label with system image**

```typescript theme={null}
Label({ title: "Favorites", systemImage: "star.fill" })
```

**Label with custom icon**

```typescript theme={null}
Label({
    title: "Profile",
    icon: Image({ url: "https://example.com/avatar.png" })
        .resizable()
        .frame({ width: 20, height: 20 })
})
```

**Label with styled title**

```typescript theme={null}
Label({
    title: Text("Important")
        .foregroundStyle(Color("red")),
    systemImage: "exclamationmark.triangle"
})
```

**Labels in a list**

```typescript theme={null}
List([
    NavigationLink(
        Label({ title: "Messages", systemImage: "message" }),
        () => MessagesView()
    ),
    NavigationLink(
        Label({ title: "Calendar", systemImage: "calendar" }),
        () => CalendarView()
    ),
    NavigationLink(
        Label({ title: "Photos", systemImage: "photo" }),
        () => PhotosView()
    ),
])
```

## ProgressView

A view that shows progress toward completion of a task, either determinate or indeterminate.

```typescript theme={null}
ProgressView(props?: { value: number; total?: number }): Component;
```

<ParamField path="value" type="number">
  The current progress value. When provided, displays a determinate progress bar.
</ParamField>

<ParamField path="total" type="number" optional default="1">
  The value that represents full completion. Defaults to `1`, so `value` is treated as a fraction (0 to 1).
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "supported" },
web: { status: "supported" },
}}
/>

**Indeterminate spinner**

When called with no arguments, displays an indeterminate spinning indicator.

```typescript theme={null}
ProgressView()
```

**Determinate progress bar**

```typescript theme={null}
ProgressView({ value: 0.7 })
```

**Custom total**

```typescript theme={null}
ProgressView({ value: 75, total: 100 })
```

**With label**

```typescript theme={null}
VStack({ spacing: 8 }, [
    HStack([
        Text("Downloading..."),
        Spacer(),
        Text("60%")
    ]),
    ProgressView({ value: 0.6 })
])
    .padding(16)
```

## See also

* [NavigationLink](/bindjs/components/NavigationLink) — navigation trigger that pairs with a Label
* [ContextMenu](/bindjs/modifiers/contextMenu) — menu triggered by long press
* [PickerStyle](/bindjs/types/PickerStyle) — picker style options
* [TextField](/bindjs/components/TextField) — single-line input alongside controls in a form
* [Text](/bindjs/components/Text) — read-only text display
