Skip to content

Visual picker

Picks one or several options shown as large cards with an icon or text. For plain choices, use Radio group or Checkbox.

Account record type

Installation

pnpm dlx shadcn@latest add @wisty/visual-picker

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
        └── VisualPickerDescription

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

Features to turn on

Wide cards

Two ways to add a contact with `variant="vertical"`: the title and description inside each card, the check in its corner.

Add a contact to this opportunity

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.

Support channels for this plan

Choose up to 2 (2 chosen)

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.

*Opportunity type
Account record type
Case assignment

API reference

PartBuilt 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

PropTypeDefaultDescription
aria-invalidboolean | "true" | "false" | "grammar" | "spelling"Set on every input, for a field with an error.
variant"default" | "vertical""default"
size"small" | "medium" | "large""medium"
namestringThe inputs' name, to submit the choice with a form. Generated when not given, so the radios still form one group.
disabledbooleanfalseDisables every card.
requiredbooleanfalseA choice must be made before the form submits: one card, or at least one with type="multiple".
formstringThe id of a form the inputs belong to, outside it.
type"single" | "multiple""single"Choose one card: radios.
valuestring | null | readonly string[]The chosen card's value, controlled. null for none.
defaultValuestring | readonly string[]
onValueChange(value: string) => void | (value: string[]) => void

VisualPickerItem

PropTypeDefaultDescription
value*stringThe value this card stands for, submitted with the form.
disabledbooleanfalseDisables this card alone.
idstringThe 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 Icon from the icon component for a Salesforce object icon.
  • No link variant (slds-box_link media cards). Use Item with an href for 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.
PropValueSLDSNotes
typesingle (added)slds-visual-picker with input type="radio"The default: one card at a time, as a radio group.
typemultiple (added)slds-visual-picker with input type="checkbox"Any number of cards, as a group of checkboxes.
sizesmall (added)slds-visual-picker_small8.5rem cards.
sizemedium (added)slds-visual-picker_mediumThe default: 12rem cards.
sizelarge (added)slds-visual-picker_large15rem cards.
variantdefault (added)slds-visual-pickerSquare cards side by side, their text under them.
variantvertical (added)slds-visual-picker_verticalWide 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__iconFills 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-checkAn accent border, and a check in the top corner.
disabledtrue (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-labelledby pointing at a FieldLegend, or with aria-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 one VisualPicker: 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 asChild or render.