Components
Multi Select
Searchable multi-select with chips, select-all, a max limit, and async option loading — built on Popover + cmdk, with a plain string[] value.
Installation
npx shadcn@latest add https://ui.saasflare.io/r/multi-select.jsonpnpm add @saasflare/uiUsage
Loading…
"use client"import * as React from "react"import { MultiSelect, type MultiSelectOption } from "@saasflare/ui"const FRAMEWORKS: MultiSelectOption[] = [ { value: "react", label: "React" }, { value: "vue", label: "Vue" }, { value: "svelte", label: "Svelte" }, { value: "solid", label: "Solid" }, { value: "angular", label: "Angular" }, { value: "qwik", label: "Qwik" }, { value: "astro", label: "Astro" },]/** * Baseline controlled multi-select: a `string[]` value held in `useState`, * chips on the trigger, search + per-row check indicators out of the box — * the thing you'd otherwise hand-wire from Combobox + Badge + state. */export function Demo() { const [value, setValue] = React.useState<string[]>(["react", "svelte"]) return ( <div className="max-w-sm"> <MultiSelect options={FRAMEWORKS} value={value} onValueChange={setValue} placeholder="Pick frameworks…" searchPlaceholder="Search frameworks…" aria-label="Frameworks" /> </div> )}Async
Loading…
"use client"import * as React from "react"import { MultiSelect, type MultiSelectOption } from "@saasflare/ui"const DIRECTORY: MultiSelectOption[] = [ { value: "ada", label: "Ada Lovelace" }, { value: "alan", label: "Alan Turing" }, { value: "grace", label: "Grace Hopper" }, { value: "linus", label: "Linus Torvalds" }, { value: "margaret", label: "Margaret Hamilton" }, { value: "dennis", label: "Dennis Ritchie" }, { value: "barbara", label: "Barbara Liskov" }, { value: "ken", label: "Ken Thompson" },]/** * Documented async recipe: `onSearchChange` flips cmdk to `shouldFilter={false}` * so the server is the filter, a debounced fake fetch toggles `loading` (the * spinner row), and the resolved `options` array is what renders. */export function Demo() { const [value, setValue] = React.useState<string[]>([]) const [options, setOptions] = React.useState<MultiSelectOption[]>(DIRECTORY) const [loading, setLoading] = React.useState(false) const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined) const handleSearch = React.useCallback((query: string) => { window.clearTimeout(timer.current) setLoading(true) timer.current = setTimeout(() => { const q = query.trim().toLowerCase() setOptions( q === "" ? DIRECTORY : DIRECTORY.filter((o) => o.label.toLowerCase().includes(q)), ) setLoading(false) }, 600) }, []) React.useEffect(() => () => window.clearTimeout(timer.current), []) return ( <div className="max-w-sm"> <MultiSelect options={options} value={value} onValueChange={setValue} loading={loading} onSearchChange={handleSearch} placeholder="Assign teammates…" searchPlaceholder="Search the directory…" emptyMessage="No teammates found." aria-label="Teammates" /> </div> )}Select All Max
Loading…
"use client"import * as React from "react"import { MultiSelect, type MultiSelectOption } from "@saasflare/ui"const PERMISSIONS: MultiSelectOption[] = [ { value: "read", label: "Read" }, { value: "write", label: "Write" }, { value: "deploy", label: "Deploy" }, { value: "billing", label: "Billing" }, { value: "admin", label: "Admin" }, { value: "audit", label: "Audit logs" },]/** * `selectAll` header + `max={3}`: over-limit options go non-interactive with a * "Max 3 selected" hint, and `maxRows={1}` collapses extra chips into a * "+N more" badge on the trigger. */export function Demo() { const [value, setValue] = React.useState<string[]>(["read", "write", "deploy"]) return ( <div className="max-w-sm"> <MultiSelect options={PERMISSIONS} value={value} onValueChange={setValue} selectAll max={3} maxRows={1} placeholder="Grant permissions…" searchPlaceholder="Search permissions…" aria-label="Permissions" /> </div> )}API Reference
Prop
Type