Visual picker
Picks one or several options shown as large cards with an icon or text. For plain choices, use Radio group or Checkbox.
Installation
The CLI installs the Radix or Base UI file to match your project.
If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.
When to use
Use it
- People choose among two to five options that are easier to tell apart with a picture or a short explanation, like a record type, a plan or how to add a contact.
- The choice starts a flow, like a Setup wizard's first step or a "New" dialog that asks what kind of record to create.
- Several options can be on at once, like the features to turn on. Use
type="multiple". - Each option needs a sentence to explain it. Use
variant="vertical", with the title and description inside the card.
Don't use it
- The options are plain words, or there are more than five. Use Radio group for one choice and Checkbox for several.
- The choice is a setting that switches on and off. Use Switch.
- People pick from a long list. Use Select, or Combobox to filter as they type.
- The buttons change a view or format in a toolbar, like Day, Week and Month. Use Toggle group.
Usage
import { FieldLegend, FieldSet } from "@/components/ui/field"
import {
VisualPicker,
VisualPickerBody,
VisualPickerDescription,
VisualPickerFigure,
VisualPickerIcon,
VisualPickerItem,
VisualPickerTitle,
} from "@/components/wisty/visual-picker"VisualPicker
└── VisualPickerItem
├── VisualPickerFigure
│ ├── VisualPickerIcon
│ ├── VisualPickerTitle
│ └── VisualPickerDescription
└── VisualPickerBody
├── VisualPickerTitle
└── VisualPickerDescriptionA card with a VisualPickerIcon fills with the accent color when it's chosen, and a check replaces the icon. A card without one keeps its content and shows the check in its corner. Put the title in VisualPickerBody under an icon card, and inside the figure of a vertical card.
Name the group from a FieldLegend, as a radio group is named. For an error, mark the FieldSet with data-invalid, set aria-invalid on VisualPicker, which passes it to every card, and point aria-describedby at the FieldError:
<FieldSet data-invalid>
<FieldLegend variant="label" id="type-legend">
Opportunity type
</FieldLegend>
<VisualPicker
name="type"
required
value={type}
onValueChange={setType}
aria-invalid
aria-labelledby="type-legend"
aria-describedby="type-error"
>
<VisualPickerItem value="new">…</VisualPickerItem>
</VisualPicker>
<FieldError id="type-error">Complete this field.</FieldError>
</FieldSet>onValueChange gets the chosen value with the default type="single", and the list of chosen values with type="multiple". With name, the choice submits with a form: one value for a single picker, one entry per chosen card for a multiple one. required asks for one card, or at least one with type="multiple".
Examples
Several choices
Features to turn on with `type="multiple"`: each card toggles on its own, and a description sits under each title.
Wide cards
Two ways to add a contact with `variant="vertical"`: the title and description inside each card, the check in its corner.
Sizes
The same two cards at `size="small"`, the default medium and `size="large"`.
Up to two
Wide checkbox cards where two of four can be chosen, the others `disabled` while two are, and a description counting the choices.
States
A required choice showing its error until one is chosen, a `disabled` card in a group, and a `disabled` group with its choice made.
API reference
| Part | Built on |
|---|---|
VisualPicker | <div> (opens in a new tab) |
VisualPickerItem | <div> (opens in a new tab) |
VisualPickerFigure | <span> (opens in a new tab) |
VisualPickerIcon | <span> (opens in a new tab) |
VisualPickerBody | <span> (opens in a new tab) |
VisualPickerTitle | <span> (opens in a new tab) |
VisualPickerDescription | <span> (opens in a new tab) |
VisualPicker
| Prop | Type | Default | Description |
|---|---|---|---|
aria-invalid | boolean | "true" | "false" | "grammar" | "spelling" | Set on every input, for a field with an error. | |
variant | "default" | "vertical" | "default" | |
size | "small" | "medium" | "large" | "medium" | |
name | string | The inputs' name, to submit the choice with a form. Generated when not given, so the radios still form one group. | |
disabled | boolean | false | Disables every card. |
required | boolean | false | A choice must be made before the form submits: one card, or at least one with type="multiple". |
form | string | The id of a form the inputs belong to, outside it. | |
type | "single" | "multiple" | "single" | Choose one card: radios. |
value | string | null | readonly string[] | The chosen card's value, controlled. null for none. | |
defaultValue | string | readonly string[] | ||
onValueChange | (value: string) => void | (value: string[]) => void |
VisualPickerItem
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | The value this card stands for, submitted with the form. | |
disabled | boolean | false | Disables this card alone. |
id | string | The input's id. Generated when not given. |
SLDS mapping
VisualPickerItem is slds-visual-picker: a visually hidden native radio or checkbox, then a label for it. size is slds-visual-picker_small, _medium or _large on every item, and variant="vertical" is slds-visual-picker_vertical. VisualPickerFigure is slds-visual-picker__figure: with a VisualPickerIcon it's the icon figure (slds-visual-picker__icon), and without one the text figure (slds-visual-picker__text), whose check is slds-visual-picker__text-check. VisualPickerBody is slds-visual-picker__body. VisualPickerTitle is slds-text-heading_small under the card and slds-text-heading_medium inside it, and VisualPickerDescription is slds-text-title. VisualPicker is the group SLDS leaves to the form element around it.
Known gaps:
- Icons and checks are lucide's, where SLDS draws utility and action icons. Pass an
Iconfrom the icon component for a Salesforce object icon. - No link variant (
slds-box_linkmedia cards). Use Item with anhreffor a list of links. - Cards in a row keep a 1rem gap when they wrap to a second row, where SLDS's wrap only on their margin.
| Prop | Value | SLDS | Notes |
|---|---|---|---|
type | single (added) | slds-visual-picker with input type="radio" | The default: one card at a time, as a radio group. |
type | multiple (added) | slds-visual-picker with input type="checkbox" | Any number of cards, as a group of checkboxes. |
size | small (added) | slds-visual-picker_small | 8.5rem cards. |
size | medium (added) | slds-visual-picker_medium | The default: 12rem cards. |
size | large (added) | slds-visual-picker_large | 15rem cards. |
variant | default (added) | slds-visual-picker | Square cards side by side, their text under them. |
variant | vertical (added) | slds-visual-picker_vertical | Wide cards stacked, the title and description inside, the check in the corner. Ignores size. |
(a card with VisualPickerIcon) | checked (added) | slds-visual-picker__figure slds-visual-picker__icon | Fills with the accent color, and a check replaces the icon. |
(a card without VisualPickerIcon) | checked (added) | slds-visual-picker__figure slds-visual-picker__text, slds-visual-picker__text-check | An accent border, and a check in the top corner. |
disabled | true (added) | slds-visual-picker input[disabled] | On VisualPicker for every card, or on VisualPickerItem for one. The card turns gray. |
Accessibility
- Name the group with
aria-labelledbypointing at aFieldLegend, or witharia-label. - Give every card text: a
VisualPickerTitle, or a description. The card's label is its name, so an icon alone leaves the card unnamed. - Each card is a native radio or checkbox. In a single picker, Tab reaches the chosen card, or the first one when none is chosen, and the arrow keys move the choice. In a multiple picker, each card is a Tab stop and Space toggles it.
- A card's whole text is its name, the description included. Keep descriptions to one sentence.
Responsive behavior
- Square cards wrap to a new row when the row is full. A vertical card is 25rem wide and narrows to fit a smaller container.
Rules
- Keep each
VisualPickerItem's cards inside oneVisualPicker: the group holds the value, the name and the size. - Use the same kind of card in a group: all with icons, or all text.
- Never pass
asChildorrender.