ui
                                
                                 ui copied to clipboard
                                
                                    ui copied to clipboard
                            
                            
                            
                        Multi select ?
Hi there ✋🏼
thanks for this amazing components !
Is there a way to select multiple data with the Select component ?
Thanks !
Unfortunately no. The <Select /> element does not support multi-select. There's an issue on the Radix UI repo I'm following too: https://github.com/radix-ui/primitives/issues/1342
👍 Would love to have a Multi Select
@its-monotype Listbox from HeadlessUI (same creators as TailwindCSS) has support for multi-select
Hey @Flo-Slv, I also wanted a multi-select, one of my colleague suggested to use a dropdown-menu with checkboxes. It worked well for my use-case. Checked it out here.
Hey @Flo-Slv, I also wanted a multi-select, one of my colleague suggested to use a dropdown-menu with checkboxes. It worked well for my use-case. Checked it out here.
Hey ! It can be a workaround until they implement a native solution ! Thanks !
https://github.com/colepeters/multiselect
What about this?
I'd love see a multi-select with labels (instead of saying something like "4 items selected"), which is incredibly valuable for adding "tags" to things - a fairly common use case
Here's the one I'm currently using (not based on tailwind-css) https://react.semantic-ui.com/modules/dropdown/
Here's a version using tailwind: https://demo-react-tailwindcss-select.vercel.app/ Github repo here: https://github.com/onesine/react-tailwindcss-select ^The downside to this one is that it doesn't look like it handles aria support / keyboard navigation
There is a headless multi-select combobox in Base UI (import X from @mui/base/useAutocomplete) which is supposed to be feature-rich:
https://mui.com/material-ui/react-autocomplete/#customized-hook
and small:
https://bundlephobia.com/package/@mui/[email protected]
maybe to consider as a base to style on top of, it could be a temporary solution for https://github.com/radix-ui/primitives/issues/1342.
In case anyone else comes to this issue looking for a solution, @mxkaske just dropped a mutli-select component built with cmdk and shadcn components.
Demo here: https://craft.mxkaske.dev/post/fancy-multi-select
Source here: https://github.com/mxkaske/mxkaske.dev/blob/main/components/craft/fancy-multi-select.tsx
In case anyone else comes to this issue looking for a solution, @mxkaske just dropped a mutli-select component built with cmdk and shadcn components.
Demo here: https://craft.mxkaske.dev/post/fancy-multi-select
Source here: https://github.com/mxkaske/mxkaske.dev/blob/main/components/craft/fancy-multi-select.tsx
This isn't accessible
@zachrip you can drop an issue in the repo here: https://github.com/mxkaske/mxkaske.dev/issues
I tweaked Headless UI Listbox component to achieve the desired UI.
Here is the example code:
import { Listbox, Transition } from '@headlessui/react';
import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons';
import React from 'react';
export default function MultiSelect() {
	const [selected, setSelected] = React.useState(['None']);
	const [options, setOptions] = React.useState<string[]>([]);
	React.useEffect(() => {
		setOptions(['None', 'Apple', 'Orange', 'Banana', 'Grapes']);
	}, []);
	return (
		<Listbox
			value={selected}
			onChange={setSelected}
			multiple>
			<div className='relative'>
				<Listbox.Button className='flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50'>
					<span className='block truncate'> {selected.map(option => option).join(', ')}</span>
					<CaretSortIcon className='h-4 w-4 opacity-50' />
				</Listbox.Button>
				<Transition
					as={React.Fragment}
					leave='transition ease-in duration-100'
					leaveFrom='opacity-100'
					leaveTo='opacity-0'>
					<Listbox.Options className='absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-popover py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm'>
						{options.map((option, optionIdx) => (
							<Listbox.Option
								key={optionIdx}
								className='relative cursor-default select-none py-1.5 pl-10 pr-4 text-sm rounded-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
								value={option}>
								{({ selected }) => (
									<>
										{option}
										{selected ? (
											<span className='absolute inset-y-0 right-2 flex items-center pl-3'>
												<CheckIcon className='h-4 w-4' />
											</span>
										) : null}
									</>
								)}
							</Listbox.Option>
						))}
					</Listbox.Options>
				</Transition>
			</div>
		</Listbox>
	);
}
Hope this works for you.
Cheers!
Hi all,
I have created component, I hope somebody will find it helpful:
import * as React from 'react'
import { cn } from "@/lib/utils"
import { Check, X, ChevronsUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
    Command,
    CommandEmpty,
    CommandGroup,
    CommandInput,
    CommandItem,
} from "@/components/ui/command"
import {
    Popover,
    PopoverContent,
    PopoverTrigger,
} from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge";
export type OptionType = {
    label: string;
    value: string;
}
interface MultiSelectProps {
    options: OptionType[];
    selected: string[];
    onChange: React.Dispatch<React.SetStateAction<string[]>>;
    className?: string;
}
function MultiSelect({ options, selected, onChange, className, ...props }: MultiSelectProps) {
    const [open, setOpen] = React.useState(false)
    const handleUnselect = (item: string) => {
        onChange(selected.filter((i) => i !== item))
    }
    return (
        <Popover open={open} onOpenChange={setOpen} {...props}>
            <PopoverTrigger asChild>
                <Button
                    variant="outline"
                    role="combobox"
                    aria-expanded={open}
                    className={`w-full justify-between ${selected.length > 1 ? "h-full" : "h-10"}`}
                    onClick={() => setOpen(!open)}
                >
                    <div className="flex gap-1 flex-wrap">
                        {selected.map((item) => (
                            <Badge
                                variant="secondary"
                                key={item}
                                className="mr-1 mb-1"
                                onClick={() => handleUnselect(item)}
                            >
                                {item}
                                <button
                                    className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
                                    onKeyDown={(e) => {
                                        if (e.key === "Enter") {
                                            handleUnselect(item);
                                        }
                                    }}
                                    onMouseDown={(e) => {
                                        e.preventDefault();
                                        e.stopPropagation();
                                    }}
                                    onClick={() => handleUnselect(item)}
                                >
                                    <X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
                                </button>
                            </Badge>
                        ))}
                    </div>
                    <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
                </Button>
            </PopoverTrigger>
            <PopoverContent className="w-full p-0">
                <Command className={className}>
                    <CommandInput placeholder="Search ..." />
                    <CommandEmpty>No item found.</CommandEmpty>
                    <CommandGroup className='max-h-64 overflow-auto'>
                        {options.map((option) => (
                            <CommandItem
                                key={option.value}
                                onSelect={() => {
                                    onChange(
                                        selected.includes(option.value)
                                            ? selected.filter((item) => item !== option.value)
                                            : [...selected, option.value]
                                    )
                                    setOpen(true)
                                }}
                            >
                                <Check
                                    className={cn(
                                        "mr-2 h-4 w-4",
                                        selected.includes(option.value) ?
                                            "opacity-100" : "opacity-0"
                                    )}
                                />
                                {option.label}
                            </CommandItem>
                        ))}
                    </CommandGroup>
                </Command>
            </PopoverContent>
        </Popover>
    )
}
export { MultiSelect }
Use it like standalone component :
import * as React from 'react'
import { MultiSelect } from from "@/components/ui/multi-select"
function Demo() {
  const [selected, setSelected] = useState<string[]>([]);
  return (
    <MultiSelect
        options={[
          {
            value: "next.js",
            label: "Next.js",
          },
          {
            value: "sveltekit",
            label: "SvelteKit",
          },
          {
            value: "nuxt.js",
            label: "Nuxt.js",
          },
          {
            value: "remix",
            label: "Remix",
          },
          {
            value: "astro",
            label: "Astro",
          },
          {
            value: "wordpress",
            label: "WordPress",
          },
          {
            value: "express.js",
            label: "Express.js",
          },
        ]}
        selected={selected}
        onChange={setSelected}
        className="w-[560px]"
      />
  )
}
or part of React Hook Form:
<FormField
    control={form.control}
    name="industry"
    render={({ field }) => (
        <FormItem>
            <FormLabel>Select Frameworks</FormLabel>
                <MultiSelect
                    selected={field.value}
                    options={[
                    {
			            value: "next.js",
			            label: "Next.js",
			          },
			          {
			            value: "sveltekit",
			            label: "SvelteKit",
			          },
			          {
			            value: "nuxt.js",
			            label: "Nuxt.js",
			          },
			          {
			            value: "remix",
			            label: "Remix",
			          },
			          {
			            value: "astro",
			            label: "Astro",
			          },
			          {
			            value: "wordpress",
			            label: "WordPress",
			          },
			          {
			            value: "express.js",
			            label: "Express.js",
			          }
                    ]}
                    {...field}
                    className="sm:w-[510px]"
                />
            <FormMessage />
        </FormItem>
    )}
 />
Hi all,
I have created component, I hope somebody will find it helpful:
import * as React from 'react' import { cn } from "@/lib/utils" import { Check, X, ChevronsUpDown } from "lucide-react" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Badge } from "@/components/ui/badge"; export type OptionType = { label: string; value: string; } interface MultiSelectProps { options: OptionType[]; selected: string[]; onChange: React.Dispatch<React.SetStateAction<string[]>>; className?: string; } function MultiSelect({ options, selected, onChange, className, ...props }: MultiSelectProps) { const [open, setOpen] = React.useState(false) const handleUnselect = (item: string) => { onChange(selected.filter((i) => i !== item)) } return ( <Popover open={open} onOpenChange={setOpen} {...props}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className={`w-full justify-between ${selected.length > 1 ? "h-full" : "h-10"}`} onClick={() => setOpen(!open)} > <div className="flex gap-1 flex-wrap"> {selected.map((item) => ( <Badge variant="secondary" key={item} className="mr-1 mb-1" onClick={() => handleUnselect(item)} > {item} <button className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" onKeyDown={(e) => { if (e.key === "Enter") { handleUnselect(item); } }} onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }} onClick={() => handleUnselect(item)} > <X className="h-3 w-3 text-muted-foreground hover:text-foreground" /> </button> </Badge> ))} </div> <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-full p-0"> <Command className={className}> <CommandInput placeholder="Search ..." /> <CommandEmpty>No item found.</CommandEmpty> <CommandGroup className='max-h-64 overflow-auto'> {options.map((option) => ( <CommandItem key={option.value} onSelect={() => { onChange( selected.includes(option.value) ? selected.filter((item) => item !== option.value) : [...selected, option.value] ) setOpen(true) }} > <Check className={cn( "mr-2 h-4 w-4", selected.includes(option.value) ? "opacity-100" : "opacity-0" )} /> {option.label} </CommandItem> ))} </CommandGroup> </Command> </PopoverContent> </Popover> ) } export { MultiSelect }Use it like standalone component :
import * as React from 'react' import { MultiSelect } from from "@/components/ui/multi-select" function Demo() { const [selected, setSelected] = useState<string[]>([]); return ( <MultiSelect options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", }, ]} selected={selected} onChange={setSelected} className="w-[560px]" /> ) }or part of React Hook Form:
<FormField control={form.control} name="industry" render={({ field }) => ( <FormItem> <FormLabel>Select Frameworks</FormLabel> <MultiSelect selected={field.value} options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", } ]} {...field} className="sm:w-[510px]" /> <FormMessage /> </FormItem> )} />
You can create a PR for this to be supported officially?
Hi all,
I have created component, I hope somebody will find it helpful:
import * as React from 'react' import { cn } from "@/lib/utils" import { Check, X, ChevronsUpDown } from "lucide-react" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Badge } from "@/components/ui/badge"; export type OptionType = { label: string; value: string; } interface MultiSelectProps { options: OptionType[]; selected: string[]; onChange: React.Dispatch<React.SetStateAction<string[]>>; className?: string; } function MultiSelect({ options, selected, onChange, className, ...props }: MultiSelectProps) { const [open, setOpen] = React.useState(false) const handleUnselect = (item: string) => { onChange(selected.filter((i) => i !== item)) } return ( <Popover open={open} onOpenChange={setOpen} {...props}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className={`w-full justify-between ${selected.length > 1 ? "h-full" : "h-10"}`} onClick={() => setOpen(!open)} > <div className="flex gap-1 flex-wrap"> {selected.map((item) => ( <Badge variant="secondary" key={item} className="mr-1 mb-1" onClick={() => handleUnselect(item)} > {item} <button className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" onKeyDown={(e) => { if (e.key === "Enter") { handleUnselect(item); } }} onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }} onClick={() => handleUnselect(item)} > <X className="h-3 w-3 text-muted-foreground hover:text-foreground" /> </button> </Badge> ))} </div> <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-full p-0"> <Command className={className}> <CommandInput placeholder="Search ..." /> <CommandEmpty>No item found.</CommandEmpty> <CommandGroup className='max-h-64 overflow-auto'> {options.map((option) => ( <CommandItem key={option.value} onSelect={() => { onChange( selected.includes(option.value) ? selected.filter((item) => item !== option.value) : [...selected, option.value] ) setOpen(true) }} > <Check className={cn( "mr-2 h-4 w-4", selected.includes(option.value) ? "opacity-100" : "opacity-0" )} /> {option.label} </CommandItem> ))} </CommandGroup> </Command> </PopoverContent> </Popover> ) } export { MultiSelect }Use it like standalone component :
import * as React from 'react' import { MultiSelect } from from "@/components/ui/multi-select" function Demo() { const [selected, setSelected] = useState<string[]>([]); return ( <MultiSelect options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", }, ]} selected={selected} onChange={setSelected} className="w-[560px]" /> ) }or part of React Hook Form:
<FormField control={form.control} name="industry" render={({ field }) => ( <FormItem> <FormLabel>Select Frameworks</FormLabel> <MultiSelect selected={field.value} options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", } ]} {...field} className="sm:w-[510px]" /> <FormMessage /> </FormItem> )} />
thank you, this is great. also wondering did anyone successfully make a form collect inputs correctly?
Unfortunately no. The
<Select />element does not support multi-select. There's an issue on the Radix UI repo I'm following too: radix-ui/primitives#1342
Is there any component that has multi-select except the dropdown? @shadcn
I created a multi input/select component but for tags that the user inputs rather than using a pre-defined list of options https://gist.github.com/enesien/03ba5340f628c6c812b306da5fedd1a4
Hi all,
I have created component, I hope somebody will find it helpful:
import * as React from 'react' import { cn } from "@/lib/utils" import { Check, X, ChevronsUpDown } from "lucide-react" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Badge } from "@/components/ui/badge"; export type OptionType = { label: string; value: string; } interface MultiSelectProps { options: OptionType[]; selected: string[]; onChange: React.Dispatch<React.SetStateAction<string[]>>; className?: string; } function MultiSelect({ options, selected, onChange, className, ...props }: MultiSelectProps) { const [open, setOpen] = React.useState(false) const handleUnselect = (item: string) => { onChange(selected.filter((i) => i !== item)) } return ( <Popover open={open} onOpenChange={setOpen} {...props}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className={`w-full justify-between ${selected.length > 1 ? "h-full" : "h-10"}`} onClick={() => setOpen(!open)} > <div className="flex gap-1 flex-wrap"> {selected.map((item) => ( <Badge variant="secondary" key={item} className="mr-1 mb-1" onClick={() => handleUnselect(item)} > {item} <button className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" onKeyDown={(e) => { if (e.key === "Enter") { handleUnselect(item); } }} onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }} onClick={() => handleUnselect(item)} > <X className="h-3 w-3 text-muted-foreground hover:text-foreground" /> </button> </Badge> ))} </div> <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-full p-0"> <Command className={className}> <CommandInput placeholder="Search ..." /> <CommandEmpty>No item found.</CommandEmpty> <CommandGroup className='max-h-64 overflow-auto'> {options.map((option) => ( <CommandItem key={option.value} onSelect={() => { onChange( selected.includes(option.value) ? selected.filter((item) => item !== option.value) : [...selected, option.value] ) setOpen(true) }} > <Check className={cn( "mr-2 h-4 w-4", selected.includes(option.value) ? "opacity-100" : "opacity-0" )} /> {option.label} </CommandItem> ))} </CommandGroup> </Command> </PopoverContent> </Popover> ) } export { MultiSelect }Use it like standalone component :
import * as React from 'react' import { MultiSelect } from from "@/components/ui/multi-select" function Demo() { const [selected, setSelected] = useState<string[]>([]); return ( <MultiSelect options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", }, ]} selected={selected} onChange={setSelected} className="w-[560px]" /> ) }or part of React Hook Form:
<FormField control={form.control} name="industry" render={({ field }) => ( <FormItem> <FormLabel>Select Frameworks</FormLabel> <MultiSelect selected={field.value} options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", } ]} {...field} className="sm:w-[510px]" /> <FormMessage /> </FormItem> )} />
I am getting an error through the selected item, I am using react hook form
 <FormField control={form.control} name="authors" render={({ field: { ...field } }) => ( <FormItem className="mb-5"> <FormLabel>Author</FormLabel> <MultiSelect selected={field.value} options={authorsData} {...field} /> </FormItem> )} />
it says that, selected is not iterable, I already check the onSelect method from CommandItem but I can't find any solution
@johnLamberts PR is still in progress, so until this is done, copy code from here, fix some imports and let me know does it work.
@johnLamberts PR is still in progress, so until this is done, copy code from here, fix some imports and let me know does it work.
I am still getting the same error, I have already check the code.
import * as React from "react";
import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
} from "@/shared/components/ui/command";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/shared/components/ui/popover";
import { cn } from "@/shared/lib/utils";
import { Check, ChevronsUpDown, X } from "lucide-react";
import { useState } from "react";
// export type OptionType = {
//   id: string;
//   value: string;
// };
export type OptionType = Record<"id" | "value", string>;
interface MultiSelectProps {
  options: Record<"id" | "value", string>[];
  selected: Record<"id" | "value", string>[];
  onChange: React.Dispatch<
    React.SetStateAction<Record<"id" | "value", string>[]>
  >;
  className?: string;
}
const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
  ({ options, selected, onChange, className, ...props }, ref) => {
    const [open, setOpen] = useState(false);
    const handleUnselect = (item: Record<"id" | "value", string>) => {
      onChange(selected.filter((i) => i.id !== item.id));
    };
    return (
      <Popover open={open} onOpenChange={setOpen} {...props}>
        <PopoverTrigger asChild>
          <Button
            ref={ref}
            role="combobox"
            variant="outline"
            aria-expanded={open}
            className={`w-full justify-between  ${
              selected?.length > 1 ? "h-full" : "h-10"
            }`}
            onClick={() => setOpen(!open)}
          >
            <div className="flex gap-1 flex-wrap">
              {selected?.map((item) => (
                <Badge
                  variant="secondary"
                  key={item.id}
                  className="mr-1 mb-1"
                  onClick={() => handleUnselect(item)}
                >
                  {item.value}
                  <button
                    className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
                    onKeyDown={(e) => {
                      if (e.key === "Enter") {
                        handleUnselect(item);
                      }
                    }}
                    onMouseDown={(e) => {
                      e.preventDefault();
                      e.stopPropagation();
                    }}
                    onClick={() => handleUnselect(item)}
                  >
                    <X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
                  </button>
                </Badge>
              ))}
            </div>
            <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
          </Button>
        </PopoverTrigger>
        <PopoverContent className="">
          <Command className={className}>
            <CommandInput placeholder="Search..." />
            <CommandEmpty>No item found.</CommandEmpty>
            <CommandGroup className="h-32 overflow-auto">
              {options.map((option) => (
                <CommandItem
                  key={option.id}
                  onSelect={() => {
                    console.log(option.value);
                    console.log(selected);
                    onChange(
                      selected?.some(
                        (item: Record<"id" | "value", string>) =>
                          item.id === option.id
                      )
                        ? selected.filter((item) => item.id !== option.id)
                        : [...selected, option]
                    );
                    setOpen(true);
                  }}
                >
                  <Check
                    className={cn(
                      "mr-2 h-4 w-4",
                      selected?.some((item) => item.id === option.id)
                        ? "opacity-100"
                        : "opacity-0"
                    )}
                  />
                  {option.value}
                </CommandItem>
              ))}
            </CommandGroup>
          </Command>
        </PopoverContent>
      </Popover>
    );
  }
);
MultiSelect.displayName = "MultiSelect";
export { MultiSelect };
``
`
btw, it the `selected` always returned me undefined, and I already check my forms well
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { MultiSelect } from "@/components/ui/multi-select"
const AuthorsSchema = z.array(
    z.record(
        z.string().trim()
    )
)
const form = useForm<z.infer<typeof AuthorsSchema>>({
    resolver: zodResolver(AuthorsSchema),
    defaultValues: {
      authors: [],
    },
  });
const onHandleSubmit = (values: z.infer<typeof AuthorsSchema>) => {
    console.log({ values })
  };
const authorsData = [
    {
      value: "author1",
      label: "Author 1",
    }, {
      value: "author2",
      label: "Author 2",
    },
    {
      value: "author3",
      label: "Author 3",
    },
    {
      value: "author4",
      label: "Author 4",
    }
  ]
<Form {...form}>
              <form
                onSubmit={form.handleSubmit(onHandleSubmit)}
                className="space-y-4"
              >
                <FormField
                  control={form.control}
                  name="authors"
                  render={({ field: { ...field } }) => (
                    <FormItem className="mb-5">
                      <FormLabel>Author</FormLabel>
                      <MultiSelect
                        selected={field.value}
                        options={authorsData}
                        {...field} />
                    </FormItem>
                  )}
                />
                <Button type="submit" className="w-full">
                  Continue
                </Button>
              </form>
            </Form>
@dinogit  Try using min-w-[var(--radix-popover-trigger-width)] for the PopoverContent to keep its width same as that of trigger.
Hi all,
I have created component, I hope somebody will find it helpful:
import * as React from 'react' import { cn } from "@/lib/utils" import { Check, X, ChevronsUpDown } from "lucide-react" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Badge } from "@/components/ui/badge"; export type OptionType = { label: string; value: string; } interface MultiSelectProps { options: OptionType[]; selected: string[]; onChange: React.Dispatch<React.SetStateAction<string[]>>; className?: string; } function MultiSelect({ options, selected, onChange, className, ...props }: MultiSelectProps) { const [open, setOpen] = React.useState(false) const handleUnselect = (item: string) => { onChange(selected.filter((i) => i !== item)) } return ( <Popover open={open} onOpenChange={setOpen} {...props}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className={`w-full justify-between ${selected.length > 1 ? "h-full" : "h-10"}`} onClick={() => setOpen(!open)} > <div className="flex gap-1 flex-wrap"> {selected.map((item) => ( <Badge variant="secondary" key={item} className="mr-1 mb-1" onClick={() => handleUnselect(item)} > {item} <button className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" onKeyDown={(e) => { if (e.key === "Enter") { handleUnselect(item); } }} onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }} onClick={() => handleUnselect(item)} > <X className="h-3 w-3 text-muted-foreground hover:text-foreground" /> </button> </Badge> ))} </div> <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-full p-0"> <Command className={className}> <CommandInput placeholder="Search ..." /> <CommandEmpty>No item found.</CommandEmpty> <CommandGroup className='max-h-64 overflow-auto'> {options.map((option) => ( <CommandItem key={option.value} onSelect={() => { onChange( selected.includes(option.value) ? selected.filter((item) => item !== option.value) : [...selected, option.value] ) setOpen(true) }} > <Check className={cn( "mr-2 h-4 w-4", selected.includes(option.value) ? "opacity-100" : "opacity-0" )} /> {option.label} </CommandItem> ))} </CommandGroup> </Command> </PopoverContent> </Popover> ) } export { MultiSelect }Use it like standalone component :
import * as React from 'react' import { MultiSelect } from from "@/components/ui/multi-select" function Demo() { const [selected, setSelected] = useState<string[]>([]); return ( <MultiSelect options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", }, ]} selected={selected} onChange={setSelected} className="w-[560px]" /> ) }or part of React Hook Form:
<FormField control={form.control} name="industry" render={({ field }) => ( <FormItem> <FormLabel>Select Frameworks</FormLabel> <MultiSelect selected={field.value} options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", } ]} {...field} className="sm:w-[510px]" /> <FormMessage /> </FormItem> )} />
is there a way to get only the values in form? instead of { label: "", value: "" }
Is there any advance in the PR? @dinogit
Hi all, I have created component, I hope somebody will find it helpful:
import * as React from 'react' import { cn } from "@/lib/utils" import { Check, X, ChevronsUpDown } from "lucide-react" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Badge } from "@/components/ui/badge"; export type OptionType = { label: string; value: string; } interface MultiSelectProps { options: OptionType[]; selected: string[]; onChange: React.Dispatch<React.SetStateAction<string[]>>; className?: string; } function MultiSelect({ options, selected, onChange, className, ...props }: MultiSelectProps) { const [open, setOpen] = React.useState(false) const handleUnselect = (item: string) => { onChange(selected.filter((i) => i !== item)) } return ( <Popover open={open} onOpenChange={setOpen} {...props}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className={`w-full justify-between ${selected.length > 1 ? "h-full" : "h-10"}`} onClick={() => setOpen(!open)} > <div className="flex gap-1 flex-wrap"> {selected.map((item) => ( <Badge variant="secondary" key={item} className="mr-1 mb-1" onClick={() => handleUnselect(item)} > {item} <button className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" onKeyDown={(e) => { if (e.key === "Enter") { handleUnselect(item); } }} onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }} onClick={() => handleUnselect(item)} > <X className="h-3 w-3 text-muted-foreground hover:text-foreground" /> </button> </Badge> ))} </div> <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-full p-0"> <Command className={className}> <CommandInput placeholder="Search ..." /> <CommandEmpty>No item found.</CommandEmpty> <CommandGroup className='max-h-64 overflow-auto'> {options.map((option) => ( <CommandItem key={option.value} onSelect={() => { onChange( selected.includes(option.value) ? selected.filter((item) => item !== option.value) : [...selected, option.value] ) setOpen(true) }} > <Check className={cn( "mr-2 h-4 w-4", selected.includes(option.value) ? "opacity-100" : "opacity-0" )} /> {option.label} </CommandItem> ))} </CommandGroup> </Command> </PopoverContent> </Popover> ) } export { MultiSelect }Use it like standalone component :
import * as React from 'react' import { MultiSelect } from from "@/components/ui/multi-select" function Demo() { const [selected, setSelected] = useState<string[]>([]); return ( <MultiSelect options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", }, ]} selected={selected} onChange={setSelected} className="w-[560px]" /> ) }or part of React Hook Form:
<FormField control={form.control} name="industry" render={({ field }) => ( <FormItem> <FormLabel>Select Frameworks</FormLabel> <MultiSelect selected={field.value} options={[ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", }, { value: "wordpress", label: "WordPress", }, { value: "express.js", label: "Express.js", } ]} {...field} className="sm:w-[510px]" /> <FormMessage /> </FormItem> )} />is there a way to get only the values in form? instead of
{ label: "", value: "" }
got it a weird error, added "use client" on top but got an error on first render on the server and then on then client
Warning: validateDOMNesting(...): <button> cannot appear as a descendant of <button>.
    at button
    at div
    at Badge (webpack-internal:///(app-pages-browser)/./components/ui/badge.tsx:29:11)
    at div
    at button
    at _c (webpack-internal:///(app-pages-browser)/./components/ui/button.tsx:41:11)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@radix-ui/react-slot/dist/index.mjs:46:23)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@radix-ui/react-slot/dist/index.mjs:20:23)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected][email protected][email protected]/node_modules/@radix-ui/react-primitive/dist/index.mjs:44:26)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@radix-ui/react-slot/dist/index.mjs:46:23)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@radix-ui/react-slot/dist/index.mjs:20:23)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected][email protected][email protected]/node_modules/@radix-ui/react-primitive/dist/index.mjs:44:26)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected][email protected][email protected]/node_modules/@radix-ui/react-popper/dist/index.mjs:80:28)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected][email protected][email protected]/node_modules/@radix-ui/react-popover/dist/index.mjs:139:29)
    at Provider (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@radix-ui/react-context/dist/index.mjs:47:28)
    at Provider (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@radix-ui/react-context/dist/index.mjs:47:28)
    at $cf1ac5d9fe0e8206$export$badac9ada3a0bdf9 (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected][email protected][email protected]/node_modules/@radix-ui/react-popper/dist/index.mjs:65:28)
    at $cb5cc270b50c6fcd$export$5b6b19405a83ff9d (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected][email protected][email protected]/node_modules/@radix-ui/react-popover/dist/index.mjs:81:29)
    at MultiSelect (webpack-internal:///(app-pages-browser)/./components/ui/multi-select.tsx:26:11)
    at div
    at PlaygroundSideBar (webpack-internal:///(app-pages-browser)/./components/playground-sidebar.tsx:29:70)
    at div
    at KnowledgeBotProvider (webpack-internal:///(app-pages-browser)/./contexts/knowledge-bot-provider.tsx:16:11)
    at div
    at InnerLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:241:11)
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/redirect-boundary.js:72:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/redirect-boundary.js:80:11)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at LoadingBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:338:11)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/error-boundary.js:110:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:152:9)
    at ScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:227:11)
    at RenderFromTemplateContext (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at OuterLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:348:11)
    at div
    at InnerLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:241:11)
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/redirect-boundary.js:72:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/redirect-boundary.js:80:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/not-found-boundary.js:54:9)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at LoadingBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:338:11)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/error-boundary.js:110:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:152:9)
    at ScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:227:11)
    at RenderFromTemplateContext (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at OuterLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/layout-router.js:348:11)
    at main
    at QueryClientProvider (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/react-query/es/react/QueryClientProvider.js:39:21)
    at Providers (webpack-internal:///(app-pages-browser)/./app/providers.tsx:14:11)
    at f (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected]/node_modules/next-themes/dist/index.module.js:8:597)
    at $ (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected]/node_modules/next-themes/dist/index.module.js:8:348)
    at ThemeProvider (webpack-internal:///(app-pages-browser)/./components/theme-provider.tsx:13:11)
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/redirect-boundary.js:72:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/redirect-boundary.js:80:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/not-found-boundary.js:54:9)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at DevRootNotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/dev-root-not-found-boundary.js:32:11)
    at ReactDevOverlay (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:294:11)
    at Router (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/app-router.js:157:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/error-boundary.js:82:9)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/error-boundary.js:110:11)
    at AppRouter (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/components/app-router.js:440:13)
    at ServerRoot (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/app-index.js:126:11)
    at RSCComponent
    at Root (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/client/app-index.js:142:11)
Great work btw!
Yeah looks like there's a  <button> nested under the Badge component that acts as the onClick handler to remove an item from the select. I changed it to an a (not sure on accessibility there), but now have this error:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Those who wants to get the data from database and follow this structure
[ { "id": 1, "name": "Small", }, { "id": 2, "name": "Medium", }, { "id": 3, "name": "Large", } ]
Also added fix for validateDOMNesting(...): <button> cannot appear as a descendant of <button> and Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef() this errors.
I've implemented all the features that React-Select offers, built entirely on Command, which is based on cmdk — no additional packages needed.
You can visit here to see demo and usage.
Features
- Multiple selection, of course.
- Async search with debounce.
- Creatable selector — create option when there is no option matched.
- Grouping functionality.
- Working with react-hook-form.
- Customize your own loading spinner and empty indicator by giving props.
- Customize style with tailwind just like shadcn-ui.
- Fixed options, maximum selected count, max input text length.
- Ability to disable the default selection of the first item. (see more about the cmdkissue)
- Expose refto get yourselectionsandinputto match your needs. For example,input.focus()
The most important: simply copy and paste — the code is yours.
I hope the only reason you'll need to dive into the source code is for Tailwind customization.
I've implemented all the features that
React-Selectoffers, built entirely onCommand, which is based oncmdk— no additional packages needed.You can visit here to see demo and usage.
Features
- Multiple selection, of course.
- Async search with debounce.
- Creatable selector.
- Grouping functionality.
- Working with
react-hook-form.- Customize your own loading spinner and empty indicator by giving props.
- Customize style with tailwind just like shadcn-ui.
- Fixed options, maximum selected count, max input text length.
- Ability to disable the default selection of the first item. (see more about the
cmdkissue)The most important: simply copy and paste — the code is yours.
I hope the only reason you'll need to dive into the source code is for Tailwind customization.
Good work!
I've implemented all the features that
React-Selectoffers, built entirely onCommand, which is based oncmdk— no additional packages needed.You can visit here to see demo and usage.
Features
* Multiple selection, of course. * **Async search with debounce**. * **Creatable selector — create option when there is no option matched**. * **Grouping functionality**. * Working with `react-hook-form`. * Customize your own loading spinner and empty indicator by giving props. * Customize style with tailwind just like shadcn-ui. * Fixed options, maximum selected count, max input text length. * Ability to disable the default selection of the first item. (see more about the `cmdk` [issue](https://github.com/pacocoursey/cmdk/issues/171)) * Expose `ref` to get your `selections` and `input` to match your needs. For example, `input.focus()`The most important: simply copy and paste — the code is yours.
I hope the only reason you'll need to dive into the source code is for Tailwind customization.
Nice! it took me a while to implement it and yours definitely works better than mine I will just go ahead and deprecate the my version and move to yours! Thank you!