All components
Multi Selector
selectsUi-Layouts component.
responsive · 600px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/multi-selector.jsonUsage
'use client';
import { useState } from 'react';
import { MultiSelect } from '@/registry/ui-layouts/multi-selector';
const frameworkOptions = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Next.js', value: 'nextjs' },
{ label: 'Nuxt', value: 'nuxt', disable: true },
];
export default function Demo() {
const [selected, setSelected] = useState<string[]>(['react']);
return (
<div className="w-full max-w-sm mx-auto p-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Frameworks
</label>
<MultiSelect
options={frameworkOptions}
onValueChange={setSelected}
defaultValue={selected}
placeholder="Choose frameworks..."
maxCount={3}
/>
{selected.length > 0 && (
<p className="mt-3 text-xs text-gray-500">
Selected: {selected.join(', ')}
</p>
)}
</div>
);
}Component source
'use client';
import { Button } from '@/registry/ui-layouts/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/registry/ui-layouts/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/registry/ui-layouts/popover';
import { cn } from '@/lib/utils';
import { CheckIcon, ChevronDown, XCircle, XIcon } from 'lucide-react';
import * as React from 'react';
/**
* Props for MultiSelect component
*/
interface MultiSelectProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* An array of option objects to be displayed in the multi-select component.
* Each option object has a label, value, and an optional icon.
*/
options: {
/** The text to display for the option. */
label: string;
/** The unique value associated with the option. */
value: string;
/** Optional icon component to display alongside the option. */
icon?: React.ComponentType<{ className?: string }>;
disable?: boolean;
}[];
/**
* Callback function triggered when the selected values change.
* Receives an array of the new selected values.
*/
onValueChange: (value: string[]) => void;
/** The default selected values when the component mounts. */
defaultValue?: string[];
/**
* Placeholder text to be displayed when no values are selected.
* Optional, defaults to "Select options".
*/
placeholder?: string;
/**
* Animation duration in seconds for the visual effects (e.g., bouncing badges).
* Optional, defaults to 0 (no animation).
*/
animation?: number;
/**
* Maximum number of items to display. Extra selected items will be summarized.
* Optional, defaults to 3.
*/
maxCount?: number;
/**
* The modality of the popover. When set to true, interaction with outside elements
* will be disabled and only popover content will be visible to screen readers.
* Optional, defaults to false.
*/
modalPopover?: boolean;
/**
* If true, renders the multi-select component as a child of another component.
* Optional, defaults to false.
*/
asChild?: boolean;
/**
* Additional class names to apply custom styles to the multi-select component.
* Optional, can be used to add custom styles.
*/
className?: string;
popoverClass?: string;
showall?: boolean;
}
export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
(
{
options,
onValueChange,
defaultValue = [],
placeholder = 'Select options',
animation = 0,
maxCount = 3,
modalPopover = false,
asChild = false,
className,
popoverClass,
showall = false,
...props
},
ref
) => {
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
setIsPopoverOpen(true);
} else if (event.key === 'Backspace' && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
}
};
const toggleOption = (option: string) => {
const newSelectedValues = selectedValues.includes(option)
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option];
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const handleClear = () => {
setSelectedValues([]);
onValueChange([]);
};
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev);
};
const clearExtraOptions = () => {
const newSelectedValues = selectedValues.slice(0, maxCount);
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const filteredOptions = options.filter((option) => !option.disable);
const toggleAll = () => {
if (selectedValues.length === filteredOptions.length) {
handleClear();
} else {
const allValues = filteredOptions.map((option) => option.value);
setSelectedValues(allValues);
onValueChange(allValues);
}
};
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
<PopoverTrigger asChild>
<Button
ref={ref}
{...props}
onClick={handleTogglePopover}
className={cn(
'flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between hover:bg-neutral-100 dark:bg-neutral-900 bg-neutral-50 dark:hover:bg-neutral-950',
className
)}
>
{selectedValues.length > 0 ? (
<div className='flex justify-between items-center w-full'>
<div className='flex flex-wrap items-center gap-1 p-1'>
{(showall ? selectedValues : selectedValues.slice(0, maxCount)).map((value) => {
const option = options.find((o) => o.value === value);
const IconComponent = option?.icon;
return (
<div
key={value}
className={cn(
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-semibold dark:bg-neutral-950 bg-neutral-200 text-primary'
)}
>
{IconComponent && <IconComponent className='h-4 w-4 mr-2' />}
{option?.label}
<XCircle
className='ml-2 h-4 w-4 cursor-pointer'
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
</div>
);
})}
{!showall && selectedValues.length > maxCount && (
<div
className={cn(
'bg-primary-foreground inline-flex items-center border px-2 py-0.5 rounded-lg text-foreground border-foreground/1 hover:bg-transparent'
)}
style={{ animationDuration: `${animation}s` }}
>
{`+ ${selectedValues.length - maxCount} more`}
<XCircle
className='ml-2 h-4 w-4 cursor-pointer'
onClick={(event) => {
event.stopPropagation();
clearExtraOptions();
}}
/>
</div>
)}
</div>
<div className='flex items-center justify-between'>
<XIcon
className='h-4 mx-2 cursor-pointer text-primary'
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
/>
{/* <Separator
orientation="vertical"
className="flex min-h-6 h-full"
/> */}
<ChevronDown className='h-4 mx-2 cursor-pointer text-primary' />
</div>
</div>
) : (
<div className='flex items-center justify-between w-full mx-auto'>
<span className='text-sm text-muted-foreground mx-3'>{placeholder}</span>
<ChevronDown className='h-4 cursor-pointer text-muted-foreground mx-2' />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className={cn('w-auto p-0', popoverClass)}
align='start'
onEscapeKeyDown={() => setIsPopoverOpen(false)}
>
<Command>
<CommandInput placeholder='Search...' onKeyDown={handleInputKeyDown} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem key='all' onSelect={toggleAll} className='cursor-pointer'>
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-xs border border-primary',
selectedValues.length === filteredOptions.length
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon className='h-4 w-4' />
</div>
<span>(Select All)</span>
</CommandItem>
{options.map((option) => {
const isSelected = selectedValues.includes(option.value);
const isDisabled = option.disable; // Check if option is disabled
return (
<CommandItem
key={option.value}
onSelect={() => !isDisabled && toggleOption(option.value)}
className={cn(
'cursor-pointer',
isDisabled && 'opacity-50 cursor-not-allowed' // Disable styling
)}
>
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-xs border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
{!isDisabled && <CheckIcon className='h-4 w-4' />}
</div>
{option.icon && (
<option.icon
className={cn('mr-2 h-4 w-4', isDisabled ? 'text-muted-foreground' : '')}
/>
)}
<span>{option.label}</span>
</CommandItem>
);
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className='flex items-center justify-between'>
{selectedValues.length > 0 && (
<>
<CommandItem
onSelect={handleClear}
className='flex-1 justify-center cursor-pointer border-r'
>
Clear
</CommandItem>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className='flex-1 justify-center cursor-pointer max-w-full'
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
);
MultiSelect.displayName = 'MultiSelect';Dependencies
lucide-react
Source: Ui-Layouts