New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Multi select ? #66
Comments
Unfortunately no. The |
👍 Would love to have a Multi Select |
@its-monotype Listbox from HeadlessUI (same creators as TailwindCSS) has support for multi-select |
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) Here's a version using tailwind: https://demo-react-tailwindcss-select.vercel.app/ |
There is a headless multi-select combobox in Base UI ( https://mui.com/material-ui/react-autocomplete/#customized-hook and small: https://bundlephobia.com/package/@mui/base@5.0.0-beta.4 maybe to consider as a base to style on top of, it could be a temporary solution for radix-ui/primitives#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 |
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>
)}
/> |
You can create a PR for this to be supported officially? |
thank you, this is great. also wondering did anyone successfully make a form collect inputs correctly? |
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 |
I am getting an error through the selected item, I am using react hook form
|
@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 { 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> |
Unfortunately this isn't screen reader accessible. Once the dropdown opens the screen reader's focus stays on the initial button that opens the dropdown. |
This code works like a charm. thanks @dinogit import * as React from "react";
import { cn } from "@/lib/utils";
import { Check, X, ChevronsUpDown, PlusCircleIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandSeparator,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Badge } from "@/components/ui/badge";
import { Input } from "../ui/input";
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));
};
const [newOption, setNewOption] = React.useState("");
const handleNewOptionEntry = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewOption(e.target.value);
};
const handleNewOptionSubmit = () => {
if (newOption) {
options.push({ label: newOption, value: newOption });
onChange(
selected.includes(newOption)
? selected.filter((item) => item !== newOption)
: [...selected, newOption]
);
setNewOption("");
setOpen(true);
}
};
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>
<CommandSeparator />
<CommandGroup>
<div className="flex gap-1">
<Input
placeholder="other tags"
value={newOption}
onChange={handleNewOptionEntry}
/>
<Button variant="ghost" onClick={handleNewOptionSubmit}>
<PlusCircleIcon />
</Button>
</div>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}
export { MultiSelect }; |
onClick is not working. when an item is clicked, it is supposed to select it or unselect but currently not working. EDIT: It's working now after changing the disable pseudo in change
|
I've ported dinogit's component into the Vue version of shadcn in case someone happens to come across this thread. It's not perfect but it works! <script setup lang="ts">
import { computed, ref, useAttrs } from 'vue';
import { cn } from '@/lib/utils';
import { Check, X, ChevronsUpDown } from "lucide-vue-next"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@/components/common/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from '@/components/ui/scroll-area';
export type OptionType = {
label: string;
value: string;
}
defineOptions({
inheritAttrs: false
})
defineProps<{
options: OptionType[]
}>();
const selected = defineModel<OptionType[]>({ default: [] });
const open = ref(false);
const attrs = useAttrs();
const buttonClass = computed(() => cn(
"w-full justify-between",
{ "h-full": selected.length > 1, "h-10": selected.length <= 1 }
));
function toggleOpen() {
open.value = !open.value;
}
function unselect(item: string) {
selected.value = selected.value.filter((i) => i.value !== item);
}
</script>
<template>
<Popover :open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="open"
:class="buttonClass"
@click="toggleOpen()"
>
<div class="flex gap-1 flex-wrap">
<Badge
variant="secondary"
v-for="item in selected"
:key="item.value"
class="mr-1 mb-1"
@click="unselect(item.value)"
>
{{ item.label }}
<button
class="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
@keydown="(e) => { if (e.key === 'Enter') unselect(item.value) }"
@mousedown.stop.prevent
@click="unselect(item.value)"
>
<X class="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
</div>
<ChevronsUpDown class="h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-full p-0">
<Command :class="cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', attrs.class as string)">
<CommandInput placeholder="Search..." />
<CommandList>
<ScrollArea class="h-36 p-1">
<CommandEmpty>No items found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="option in options"
:key="option.value"
:value="option.label"
class="hover:cursor-pointer"
@select="() => {
if (selected.map((r) => r.value).includes(option.value)) {
unselect(option.value);
} else {
selected = [...selected, option];
}
open = true;
}"
>
<Check
v-if="selected.map((r) => r.value).includes(option.value)"
:class="cn('mr-2 h-4 w-4', { 'opacity-100': selected.map((r) => r.value).includes(option.value), 'opacity-0': !selected.map((r) => r.value).includes(option.value)})"
/>
{{ option.label }}
</CommandItem>
</CommandGroup>
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template> |
I got an error. Warning: validateDOMNesting(...): <button> cannot appear as a descendant of <button>.
at button
at div
at Badge (http://localhost:5173/src/components/ui/badge.tsx:35:18)
at div
at button
at _c (http://localhost:5173/src/components/ui/button.tsx:47:11) |
I've assembled a multi-select component using the native shadcn's components. It's fully in line with design and integrates seamlessly into shadcn's ecosystem. Please, try it out and share your thoughts. |
Hey, yes, that looks good! The previous error was resolved by using divs as buttons, although this approach lacks semantic value. However, it works. Can we improve keyboard accessibility? I couldn't navigate through the component using my keyboard. |
OK, I’ll finalize the component for full interaction with the keyboard. |
Neat! Will try it |
to @timwehrle Hi,
Try it here: https://shadcn-multi-select-component.vercel.app/ |
@sersavan I can't get your component to work. Options are disabled for some reason for me even though I have identical multi-select component as yours 🤔 |
to @snufkind I've fixed issue with cmdk, now you can use latest version as well |
Really awesome work with this component @sersavan ! I was looking into the aria-selected of each of the CommandItems and seems they are not following what's really selected, as we are controlling in the component the state of each item ourselves. Did you also noticed that ? |
@joaopedrodcf |
Thanks @sersavan. I really like the UI, but I'd prefer to use it with numerical values. Could you assist me with that? |
@cbptamtom |
and in my form. I just use warehouse_id is a number array [number] |
@cbptamtom |
Thanks @sersavan. I've fixed |
@sersavan You're doing god's work here 😅 Only thing that would make this absolutely perfect is replacing the breadcrumb's with a text. For example "6 options selected" so that the element doesn't overflow or grow horizontally. |
@snufkind |
@sersavan First thanks. I am unable to add my own style to the component. Can you help me to address this issue. |
Hi, I've added a few props, including |
Where does it updated? |
@zekageri |
Hello any accessible solution for app-index.js:33 Warning: In HTML, button cannot be a descendant of button. |
not working !! Unexpected Application Error! |
@AhmedBelkadi |
Hi there ✋🏼
thanks for this amazing components !
Is there a way to select multiple data with the Select component ?
Thanks !
The text was updated successfully, but these errors were encountered: