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?
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`. |
columns | 1 | 2 | 3 | 4 | cards: 2 · 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. |
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`. |
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. |
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. |