Multi Select - React Hook Form
A Multi Select component that uses react-hook-form Controller.
Installation
npx shadcn@latest add /registry/rhf-multi-select.json
Manual Installation
This component is dependent on floating-button component. Please add that component first before adding this one.
1. Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils";
import { FloatingLabelButon } from "@/components/ui/buttons/floating-label-button";
import { ChevronsUpDown, Square, SquareCheckBig } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
// ----------------------------------------------------------------------
type SelectOption = {
label: string;
value: string;
};
interface CustomRHFMultiSelectProps<TFieldValues extends FieldValues>
extends Omit<React.ComponentPropsWithoutRef<typeof Controller>, "name" | "control" | "render"> {
name: FieldPath<TFieldValues>;
btnProps?: Omit<React.ComponentPropsWithoutRef<typeof FloatingLabelButon>, "value" | "label">;
popoverProps?: React.ComponentPropsWithoutRef<typeof PopoverContent>;
helperText?: string;
containerClass?: string;
label: string;
options: SelectOption[];
}
export default function CustomRHFMultiSelect<TFieldValues extends FieldValues>({
name,
label,
helperText,
defaultValue,
rules,
containerClass,
btnProps = {
variant: "outline",
size: "md",
className: "w-full justify-between",
},
popoverProps,
options,
}: CustomRHFMultiSelectProps<TFieldValues>) {
const btnRef = React.useRef<React.ElementRef<"button">>(null);
const [open, setOpen] = React.useState(false);
const { control } = useFormContext();
const handleValueChange = React.useCallback((value: string, inputValue: string, onChange: (...event: any[]) => void) => {
const values = inputValue.trim() === "" ? [] : inputValue.split(",");
const valIdx = values.findIndex((val) => val === value);
if (valIdx === -1) {
values.push(value);
} else {
values.splice(valIdx, 1);
}
onChange(values.join(","));
setOpen(false);
}, []);
const popOverStyle = !!btnRef.current ? { width: btnRef.current.clientWidth } : {};
return (
<Controller
name={name}
control={control}
defaultValue={defaultValue}
rules={rules}
render={({ field, fieldState: { error } }) => {
return (
<div className={containerClass}>
<Popover>
<PopoverTrigger asChild>
<FloatingLabelButon
role={name + "-popover"}
aria-expanded={open}
{...btnProps}
ref={btnRef}
label={label}
value={field.value}
endIcon={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
error={!!error}
/>
</PopoverTrigger>
<PopoverContent
{...popoverProps}
style={popOverStyle}
className={cn("p-0 space-y-0 w-full min-w-[--radix-popper-anchor-width]", popoverProps?.className)}>
{options.map((option, index) => {
const selected = (field.value as string).split(",").findIndex((val) => val === option.value) !== -1;
return (
<div
key={index}
onClick={() => handleValueChange(option.value, field.value, field.onChange)}
className={cn(
"flex items-center rounded-md py-2 px-1.5 cursor-pointer select-none hover:bg-accent dark:hover:bg-accent/10",
{
"font-medium bg-accent dark:bg-accent/10": selected,
},
)}>
{selected ? <SquareCheckBig className="h-4 w-4" /> : <Square className="h-4 w-4" />}
<span className="text-sm ml-2 whitespace-nowrap">{option.label}</span>
</div>
);
})}
</PopoverContent>
</Popover>
<div className="space-y-2 mt-2">
{!!helperText && <p className="text-sm text-muted-foreground ml-1.5">{helperText}</p>}
{!!error && <p className="text-sm text-error ml-1.5">{error?.message}</p>}
</div>
</div>
);
}}
/>
);
}
2. Add this to your @/components/ui/hook-form/index.tsx
file for easy import.
export { default as RHFMultiSelect } from "./rhf-multi-select";
Shadcn
This multi-select component is built on top of the excellent foundation provided by Shadcn/UI popover component.
For a deeper dive into the core concepts and building blocks, check out the original Shadcn popover component.