Skip to content

Instantly share code, notes, and snippets.

@aryomuzakki
Last active June 29, 2025 14:47
Show Gist options
  • Select an option

  • Save aryomuzakki/f377cf41db9b1b4208b493ada6fd53c9 to your computer and use it in GitHub Desktop.

Select an option

Save aryomuzakki/f377cf41db9b1b4208b493ada6fd53c9 to your computer and use it in GitHub Desktop.
import { MultiSelect } from '@/components/ui/multi-select'
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
]
function MyComponent() {
const [selected, setSelected] = React.useState<string[]>([])
return (
<MultiSelect
options={options}
selected={selected}
onChange={setSelected}
placeholder="Select fruits..."
emptyText="No fruits found."
/>
)
}
/**
* (https://github.com/shadcn-ui/ui/issues/948#issuecomment-2679374754)
*/
"use client"
import * as React from "react"
import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
export type Option = {
value: string
label: string
}
interface MultiSelectProps {
options: Option[]
selected: string[]
onChange: (selected: string[]) => void
placeholder?: string
emptyText?: string
className?: string
}
export function MultiSelect({
options,
selected,
onChange,
placeholder = "Select options...",
emptyText = "No options found.",
className,
}: MultiSelectProps) {
const [open, setOpen] = React.useState(false)
const handleSelect = React.useCallback(
(value: string) => {
const updatedSelected = selected.includes(value)
? selected.filter((item) => item !== value)
: [...selected, value]
onChange(updatedSelected)
},
[selected, onChange],
)
const selectedLabels = React.useMemo(
() =>
selected
.map((value) => options.find((option) => option.value === value)?.label)
.filter(Boolean)
.join(", "),
[selected, options],
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between", className)}
>
<span className="truncate">{selected.length > 0 ? selectedLabels : placeholder}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="max-h-[var(--radix-popover-content-available-height)] w-[var(--radix-popover-trigger-width)] p-0">
<Command>
<CommandInput placeholder="Search options..." className="h-9" />
<CommandList>
<CommandEmpty>{emptyText}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem key={option.value} value={option.value} onSelect={() => handleSelect(option.value)}>
{option.label}
<Check
className={cn("ml-auto h-4 w-4", selected.includes(option.value) ? "opacity-100" : "opacity-0")}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
import { MultiSelect } from '@/components/ui/multi-select'
import { useForm, Controller } from 'react-hook-form'
const options = [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'angular', label: 'Angular' },
]
function MyForm() {
const { control, handleSubmit } = useForm()
const onSubmit = (data) => {
console.log(data.frameworks)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="frameworks"
control={control}
defaultValue={[]}
render={({ field }) => (
<MultiSelect
options={options}
selected={field.value}
onChange={field.onChange}
placeholder="Select frameworks..."
emptyText="No frameworks found."
/>
)}
/>
<button type="submit">Submit</button>
</form>
)
}
@aryomuzakki
Copy link
Author

  • fix popover width to match trigger button width
  • fix height to not overflow available height
<PopoverContent 
  className='max-h-[var(--radix-popover-content-available-height)] w-[var(--radix-popover-trigger-width)]'
>...</PopoverContenr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment