# Field: Button Card (category: toggle)

URL: https://docs.forms.saastro.io/docs/button-card

<FieldPreview name="button-card" description="Interactive demo of button card field" />

## Overview

The button card field presents options as rich, card-style buttons. Each option pairs a bold title (`text`) with an optional description and icon. Ideal for plan selection, feature comparison, or any choice where visual hierarchy helps users decide.

By default a button card is single-select (the value is a `string`). Set `multiple: true` to turn the cards into toggles and collect a `string[]`.

## Usage

### Basic Button Card

```tsx

const config = FormBuilder.create('form')
  .addField('plan', (f) =>
    f
      .type('button-card')
      .label('Select a Plan')
      .buttonCardOptions([
        {
          value: 'free',
          text: 'Free',
          description: 'Perfect for getting started',
          icon: 'lucide:gift',
        },
        {
          value: 'pro',
          text: 'Pro',
          description: 'For growing teams',
          icon: 'lucide:star',
        },
        {
          value: 'enterprise',
          text: 'Enterprise',
          description: 'Custom solutions',
          icon: 'lucide:building-2',
        },
      ])
      .schema(z.string().min(1, 'Please select a plan')),
  )
  .addStep('main', ['plan'])
  .build();
```

Single-select button cards hold a `string` value, so validate them with a Zod string schema as above — see [Validation](#validation) for why `.required()` doesn't fit this field type.

### Multi-select Cards

Multi-select is enabled by the `multiple` flag (`.multiple(true)` in the builder). The field value then becomes a `string[]`, and clicking a card toggles it in and out of the array. Always pair it with `.value([])` — the implicit initial value is `''`, not an empty array:

```tsx
.addField('features', (f) =>
  f.type('button-card')
    .label('Select Features')
    .buttonCardOptions([
      { value: 'analytics', text: 'Analytics', description: 'Track metrics', icon: 'lucide:bar-chart-3' },
      { value: 'api', text: 'API Access', description: 'REST & GraphQL', icon: 'lucide:plug' },
      { value: 'support', text: 'Priority Support', description: '24/7 help', icon: 'lucide:message-circle' },
    ])
    .multiple(true)
    .value([]) // start with an empty array
    .optional()
)
```

### JSON Configuration

```json
{
  "type": "button-card",
  "label": "Select a Plan",
  "options": [
    {
      "value": "free",
      "text": "Free",
      "description": "Perfect for getting started",
      "icon": "lucide:gift"
    },
    {
      "value": "pro",
      "text": "Pro",
      "description": "For growing teams",
      "icon": "lucide:star"
    }
  ]
}
```

For multi-select in JSON, add `"multiple": true` at the field level, start the value at an empty array, and validate with `minItems`:

```json
{
  "type": "button-card",
  "label": "Select Features",
  "multiple": true,
  "value": [],
  "options": [
    { "value": "analytics", "text": "Analytics", "icon": "lucide:bar-chart-3" },
    { "value": "api", "text": "API Access", "icon": "lucide:plug" }
  ],
  "schema": { "minItems": 1, "minItemsMessage": "Select at least one feature" }
}
```

## Props

| Prop               | Type                                                  | Default | Description                                          |
| ------------------ | ----------------------------------------------------- | ------- | ---------------------------------------------------- |
| `type`             | `'button-card'`                                       | -       | Field type (required)                                |
| `label`            | `string`                                              | -       | Group label                                          |
| `helperText`       | `string`                                              | -       | Help text below field                                |
| `options`          | `ButtonCardOption[]`                                  | -       | Array of card options                                |
| `multiple`         | `boolean`                                             | `false` | Multi-select mode — value becomes `string[]`         |
| `cardLayout`       | `'horizontal' \| 'vertical'`                          | `'horizontal'` | `'vertical'` stacks icon above text, centered — ideal for logo pickers (`<img>` as `icon`) |
| `value`            | `string \| string[]`                                  | -       | Initial value. Set `[]` when `multiple` is `true` (the implicit default is `''`) |
| `optionsClassName` | `string`                                              | -       | CSS classes for the cards container (default `grid grid-cols-1 gap-2`) |
| `columns`          | `Partial<Record<Breakpoint, number>>`                 | -       | Grid columns the field spans, by breakpoint          |
| `disabled`         | `boolean \| function \| ConditionGroup`               | `false` | Disable all cards                                    |
| `hidden`           | `boolean \| function \| ConditionGroup \| Responsive` | `false` | Hide the field                                       |

### ButtonCardOption Object

| Property      | Type                      | Description                                                          |
| ------------- | ------------------------- | -------------------------------------------------------------------- |
| `value`       | `string`                  | Selection value (required)                                            |
| `text`        | `string`                  | Card title (required)                                                 |
| `description` | `string`                  | Optional card description                                             |
| `icon`        | `ReactNode`               | Optional icon: an Iconify icon name (e.g. `'lucide:star'`) or a React element |
| `iconProps`   | `Record<string, unknown>` | Optional props forwarded to the icon (e.g. `className`)               |

Individual cards cannot be disabled — the `disabled` prop applies to the whole field.

## Validation

Validation depends on the mode. Serializable rules (`ValidationRules` — what `.required()` and JSON `"schema"` objects produce) compile button-card to an **array** schema, which matches the `string[]` value of multi-select mode but not the `string` value of single-select mode. Use a Zod schema for single-select, and `minItems` for multi-select. See the [validation guide](/docs/validation) for the full rules reference.

### Required Selection (single-select)

Pass a Zod schema directly:

```tsx

.addField('tier', (f) =>
  f.type('button-card')
    .label('Select Tier')
    .buttonCardOptions([...])
    .schema(z.string().min(1, 'Please select a tier'))
)
```

### At Least One Card (multi-select)

```tsx
.addField('features', (f) =>
  f.type('button-card')
    .label('Select Features')
    .buttonCardOptions([...])
    .multiple(true)
    .value([])
    .itemCount(1) // error message: "Select at least 1"
)
```

In JSON, use `"schema": { "minItems": 1, "minItemsMessage": "Select at least one" }`.

## Styling

### Custom Classes

```tsx
.addField('plan', (f) =>
  f.type('button-card')
    .label('Select a Plan')
    .buttonCardOptions([...])
    .schema(z.string().min(1, 'Please select a plan'))
    .classNames({
      wrapper: 'bg-muted/30 p-4 rounded-lg',
      label: 'text-lg font-semibold mb-3',
      error: 'text-destructive text-sm',
    })
)
```

### Cards Layout with `optionsClassName`

Control card arrangement:

```json
{
  "type": "button-card",
  "label": "Select Plan",
  "optionsClassName": "grid grid-cols-1 md:grid-cols-3 gap-4",
  "options": [...]
}
```

### Responsive Layout

```tsx
.addField('plan', (f) =>
  f.type('button-card')
    .label('Select a Plan')
    .buttonCardOptions([...])
    .schema(z.string().min(1, 'Please select a plan'))
    .columns({ default: 12, md: 8 })
)
```

### JSON Styling

```json
{
  "type": "button-card",
  "label": "Select Plan",
  "wrapper_className": "bg-muted/30 p-6 rounded-xl",
  "label_className": "text-xl font-bold",
  "optionsClassName": "grid grid-cols-1 md:grid-cols-3 gap-6",
  "columns": { "default": 12 }
}
```

## Related Fields

- [Radio](/docs/radio) - Simple radio options
- [Button Radio](/docs/button-radio) - Compact button selection
- [Button Checkbox](/docs/button-checkbox) - Button-style multi-select
- [Select](/docs/select) - Dropdown selection

## Accessibility

- Keyboard navigation between cards
- Focus visible states
- Selected state communicated via styling and button variants
