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
import { FormBuilder } from '@saastro/forms';
import { z } from 'zod';
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 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:
.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
{
"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:
{
"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 for the full rules reference.
Required Selection (single-select)
Pass a Zod schema directly:
import { z } from 'zod';
.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)
.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
.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:
{
"type": "button-card",
"label": "Select Plan",
"optionsClassName": "grid grid-cols-1 md:grid-cols-3 gap-4",
"options": [...]
}
Responsive Layout
.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
{
"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 - Simple radio options
- Button Radio - Compact button selection
- Button Checkbox - Button-style multi-select
- Select - Dropdown selection
Accessibility
- Keyboard navigation between cards
- Focus visible states
- Selected state communicated via styling and button variants