One component, two variants — pass `type="cards"` for bordered rows with thumbnails or `type="chip-group"` for compact h-9 pills.
Pick the design services you need
Your selections
1 of 6npx shadcn@latest add https://sdk-components.thesqd.com/r/multi-select.jsonimport { MultiSelect } from "@/components/ui/multi-select";import * as React from "react";
import { MultiSelect } from "@/components/ui/multi-select";
const PRODUCTS = [
{ value: "sermon-design", title: "Sermon Design", caption: "Slides + graphics for your weekly series.", image: "https://picsum.photos/seed/sermon/112" },
{ value: "social-media", title: "Social Media", caption: "Templates tuned for IG / TikTok / etc.", image: "https://picsum.photos/seed/social/112" },
{ value: "event-design", title: "Event Design", caption: "Visual branding for events.", image: "https://picsum.photos/seed/event/112" },
];
export function Example() {
const [value, setValue] = React.useState<string[]>([]);
return (
<MultiSelect
type="cards"
aria-label="Design services"
value={value}
onChange={setValue}
options={PRODUCTS}
/>
);
}MultiSelect
├── type="cards" | "chip-group" (variant discriminator)
├── options (shape depends on `type`)
│ ├── cards → { value, title, caption?, image?, swatch?, lastUsed?, badge?, disabled? }
│ └── chip-group → { value, label, disabled? }
├── value / defaultValue (string[] — works the same for both types)
├── onChange ((next: string[]) => void)
├── className (override the layout — chip-group defaults to grid-cols-3, cards to sm:grid-cols-2)
└── aria-label (group accessible label)Pick the design services you need
Your selections
1 of 6Pick the design services you need
What kind of reel are you requesting?
Which of these styles feels right?
Which of these feels right?
Which of these styles feels right?
Pick the ones you like, and say what you like about each — that's what your designer reads.
Output
{
"value": [
"minimal",
"collage"
],
"detailValues": {}
}Pick the services you need
MultiSelect
| Prop | Type | Default | Description |
|---|---|---|---|
type | "cards" | "chip-group" | — | Selects the variant. `cards` renders bordered rows with a swatch + title + description and a `+` / ✓ toggle in the corner. `chip-group` renders compact h-9 pills. |
options | MultiSelectCardOption[] | MultiSelectChipOption[] | — | Items to render — shape depends on `type`. |
value | string[] | — | Controlled selected values. |
defaultValue | string[] | — | Uncontrolled initial values. |
onChange | (next: string[]) => void | — | Selection change handler. |
aria-label | string | — | Accessible label for the group. |
className | string | — | Override the layout. `cards` defaults to `sm:grid-cols-2`. `chip-group` defaults to `grid grid-cols-3` — pass `flex flex-wrap gap-2` for a wrap layout. |
wrap | boolean | false | Chip-group only. Flow chips in a wrapping inline row sized to each label instead of a fixed grid. Wins over `columns`. |
layout | "row" | "tile" | "row" | Cards only. `tile` makes the image the whole card (16:9, `+` / ✓ over its corner) and grows any `requiresDetail` field out of the tile's bottom edge, at any `columns`. |
bento | boolean | false | `layout="tile"` only. A bento grid for images of mixed shapes: every image keeps its own aspect ratio at column width, tiles go to the shortest column, and each column is scaled a few percent to one height — a flush rectangle with no holes. Arrow keys move to the nearest tile on screen; one column on a phone. The shape is the option's `aspectRatio`, or measured as the image loads. Ignored when any option `requiresDetail`. |
indicator | "plus" | "radio" | "plus" | Cards only. The corner mark on each card: a `+` / ✓ button, or a radio dot for single-choice pickers (RadioGroup `type="image"` uses `radio`). |
columns | 1 | 2 | 3 | 4 | cards: 2 · tiles: 3 · chips: 3 | Number of equal-width columns. Cards apply it at the `sm:` breakpoint; chip-group applies it at all sizes (ignored when `wrap` is set). |
sections | { key, label, icon? }[] | — | Cards only. When provided, options group by `option.section === section.key`. Each header renders a FeaturedIcon + label + hairline divider. Orphan options fall into a trailing implicit "Other" group. |
detailValues | Record<string, string> | — | Cards only. Controlled `option.value → detail text` map for options flagged `requiresDetail`. |
defaultDetailValues | Record<string, string> | — | Cards only. Uncontrolled initial detail-text map. |
dictation | boolean | true | Cards only. Show the mic (browser dictation) on revealed detail fields. Pass `false` to hide it. |
onDetailChange | (next: Record<string, string>) => void | — | Cards only. Fires when any revealed detail textarea changes. |
MultiSelectCardOption (when type="cards")
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Stable value for this option. |
title | ReactNode | — | Primary label rendered next to the swatch/image. |
caption | ReactNode | — | Optional muted helper line under the title. |
image | string | — | Image URL rendered as the leading thumbnail. `.mp4`/`.webm`/`.mov`/`.m4v`/`.ogv` swap to a muted/autoplay/loop `<video>`. |
imageAlt | string | — | Alt text for `image`. |
aspectRatio | number | — | The image's width ÷ height, for `bento` tiles. Left out, the tile measures its image as it loads (a plain 16:9 cell until then). |
swatch | ReactNode | — | Custom JSX (gradient, icon, …) — wins over `image`. |
lastUsed | boolean | false | Renders a built-in amber "Last used" pill floated on the card's top border. Pass `lastUsed: true` — no `<Badge>` import needed. |
badge | ReactNode | — | Optional custom pill floated on the card's top border. Renders alongside the built-in `lastUsed` pill when both are set. |
section | string | — | Group key matching a `sections[*].key`. Only used when `sections` is set. |
requiresDetail | boolean | false | Selecting this card reveals a required textarea nested beneath it, tied back with an elbow connector. Only honored at `columns` 1 or 2 — except with `layout="tile"`, where the field grows inside the tile. |
detailLabel | ReactNode | — | Label above the revealed detail textarea. Defaults to "Provide more details". |
detailPlaceholder | string | — | Placeholder for the revealed detail textarea. |
disabled | boolean | false | Disable this card. |
MultiSelectChipOption (when type="chip-group")
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Stable value for this option. |
label | ReactNode | — | Visible chip label. |
disabled | boolean | false | Disable this chip. |