Select Dropdown + Searchbar + Clearable (React & Shadcn)

Shadcn provides a fantastic set of beautiful UI components right out of the box. One of the most commonly used components is a selector. However, the component from shadcn (which is based on Radix UI) lacks certain features, such as search functionali…


This content originally appeared on DEV Community and was authored by Vic Ong

Shadcn provides a fantastic set of beautiful UI components right out of the box. One of the most commonly used components is a selector. However, the component from shadcn (which is based on Radix UI) lacks certain features, such as search functionality and the ability to clear selected options.

In this guide, I'll be implementing a custom select dropdown component that supports searching and clearing options.

Select Dropdown

Let's start with a list of options:

const options = [
  { value: "apple": label: "Apple" },
  { value: "banana": label: "Banana" },
  { value: "avocado": label: "Avocado" },
  // ...
];

First, I will create a basic dropdown using <Command> and <Popover> that:

  • Displays a list of options
  • Shows a checkmark for the selected option
  • Includes a close button
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
  Command,
  CommandGroup,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";

export type SelectOption = {
  value: string;
  label: string;
};

export const InputSelect: React.FC<{
  options: SelectOption[];
  value?: string;
  onValueChange?: (v: string) => void;
  className?: string;
  style?: React.CSSProperties;
  children: React.ReactNode;
}> = ({
  options,
  value = "",
  onValueChange,
  className,
  children,
}) => {
  const [selectedValue, setSelectedValue] = React.useState<string>(value);
  const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);

  const onOptionSelect = (option: string) => {
    setSelectedValue(option);
    onValueChange?.(option);
    setIsPopoverOpen(false);
  };

  return (
    <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
      <PopoverTrigger asChild>
        {children}
      </PopoverTrigger>
      <PopoverContent className={cn("w-auto p-0", className)} align="start">
        <Command>
          <CommandList className="max-h-[unset] overflow-y-hidden">
            <CommandGroup className="max-h-[20rem] min-h-[10rem] overflow-y-auto">
              {options.map((option) => {
                const isSelected = selectedValue === option.value;
                return (
                  <CommandItem
                    key={option.value}
                    onSelect={() => onOptionSelect(option.value)}
                    className="cursor-pointer"
                  >
                    <div
                      className={cn(
                        "mr-1 flex h-4 w-4 items-center justify-center",
                        isSelected ? "text-primary" : "invisible"
                      )}
                    >
                      <CheckIcon className="w-4 h-4" />
                    </div>
                    <span>{option.label}</span>
                  </CommandItem>
                );
              })}
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup>
              <div className="flex items-center justify-between">
                <CommandItem
                  onSelect={() => setIsPopoverOpen(false)}
                  className="justify-center flex-1 max-w-full cursor-pointer"
                >
                  Close
                </CommandItem>
              </div>
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
};
InputSelect.displayName = "InputSelect";

Add Search Functionality

To enhance usability, I'll integrate <CommandInput> for built-in search capabilities and <CommandEmpty> to display a message when no results are found.

export const InputSelect = () => {
  // ...
  return (
    <Popover {...}>
      <PopoverTrigger {...} />
      <PopoverContent {...}>
        <Command>
          <CommandInput placeholder="Search..." />
          <CommandList {...}>
            <CommandEmpty>No results found.</CommandEmpty>
            <CommandGroup {...}>
              // ...
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup>
              // ...
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
};

Adding a Clear Option

I also want to provide a button to clear the selected value when one is chosen.

import { Separator } from "@/components/ui/separator";

export const InputSelect: React.FC<{ ... }> = ({ ... }) => {
  // ...

  const onClearAllOptions = () => {
    setSelectedValue("");
    onValueChange?.("");
    setIsPopoverOpen(false);
  };

  return (
    <Popover {...}>
      <PopoverTrigger {...} />
      <PopoverContent {...}>
        <Command>
          <CommandInput {...} />
          <CommandList {...}>
            <CommandEmpty {...} />
            <CommandGroup {...}>
              // ...
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup>
              <div className="flex items-center justify-between">
                {selectedValue && (
                  <>
                    <CommandItem
                      onSelect={onClearAllOptions}
                      className="justify-center flex-1 cursor-pointer"
                    >
                      Clear
                    </CommandItem>
                    <Separator
                      orientation="vertical"
                      className="flex h-full mx-2 min-h-6"
                    />
                  </>
                )}
                <CommandItem
                  onSelect={() => setIsPopoverOpen(false)}
                  className="justify-center flex-1 max-w-full cursor-pointer"
                >
                  Close
                </CommandItem>
              </div>
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
};

So far so good, now I have a dropdown looking like so:

input-select-1

Add Dropdown Trigger

Now, for the last step, I can add a trigger to toggle open/close on the dropdown. I could just use a <Button> for that.

import { Separator } from "@/components/ui/separator";

export const InputSelect: React.FC<{ ... }> = ({ ... }) => {
  // ...

  const onClearAllOptions = () => {
    setSelectedValue("");
    onValueChange?.("");
    setIsPopoverOpen(false);
  };

  return (
    <Popover {...}>
      <PopoverTrigger asChild>
        <Button
          onClick={() => setIsPopoverOpen((prev) => !prev)}
          variant="outline"
          type="button"
          className="flex h-11 w-full items-center justify-between p-1 [&_svg]:pointer-events-auto"
        >
          {selectedValue ? (
            <div className="flex items-center justify-between w-full">
              <div className="flex items-center justify-between w-full">
                <div className="flex items-center px-3 text-foreground">
                  {options.find((v) => v.value === selectedValue)?.label}
                </div>
                <div className="flex items-center justify-between">
                  {selectedValue && (
                    <>
                      <X
                        className="mx-1 h-4 cursor-pointer text-muted-foreground"
                        onClick={(e) => {
                          e.stopPropagation();
                          onClearAllOptions();
                        }}
                      />
                      <Separator orientation="vertical" className="flex h-full min-h-6" />
                    </>
                  )}
                  <ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
                </div>
              </div>
              <div className="flex items-center justify-between">
                {selectedValue && (
                  <>
                    <X
                      className={cn(
                        "mx-1 h-4 cursor-pointer text-muted-foreground",
                      )}
                      onClick={(e) => {
                        e.stopPropagation();
                        onClearAllOptions();
                      }}
                    />
                    <Separator orientation="vertical" className="flex h-full min-h-6" />
                  </>
                )}
                <ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
              </div>
            </div>
          ) : (
            <div className="flex items-center justify-between w-full mx-auto">
              <span className="px-3 text-sm text-muted-foreground">{placeholder}</span>
              <ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
            </div>
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent {...}>
        // ...
      </PopoverContent>
    </Popover>
  );
};

With these enhancements, I've built a fully functional <InputSelect> component that supports searching and clearing selected options. I can now just use this component anywhere in my app like so:

import * as React from "react";
import { InputSelect } from "path/to/input-select";

export const MyAwesomeComponent = () => {
  const [value, setValue] = React.useState("apple");

  const options = [...];

  return (
    <div>
      <InputSelect
        options={options}
        value={value}
        onValueChange{(v) => setValue(v)}
      />
    </div>
  );
}

input-select-2

Extend customizability (optional)

In React, I can technically pass any children prop to a component so long as it renders as a functional component.

For example:

// By default React accepts children prop as a ReactNode type
export const CompA = ({ children: React.ReactNode }) => (
  <div>{children}</div>
);
export const CompB = () => <CompA>hello</CompA>;

// We can also modify to accept a function!
export const CompA = ({ children: (v: { value: string; }) => React.ReactNode }) => (
  <div>{children({ value: "foo" })}</div>
);
export const CompB = () => <CompA>{(prop) => <div>{prop.value}</div>}</CompA>;

So, for the <InputSelect>, I can extract the InputSelectTrigger out as a separate component to provide additional customizability

export interface InputSelectProvided {
  options: SelectOption[];
  onValueChange?: (v: string) => void;
  selectedValue: string;
  setSelectedValue: React.Dispatch<React.SetStateAction<string>>;
  isPopoverOpen: boolean;
  setIsPopoverOpen: React.Dispatch<React.SetStateAction<boolean>>;
  onOptionSelect: (v: string) => void;
  onClearAllOptions: () => void;
}

export const InputSelect: React.FC<{
  options: SelectOption[];
  value?: string;
  onValueChange?: (v: string) => void;
  className?: string;
  style?: React.CSSProperties;
  children: (v: InputSelectProvided) => React.ReactNode;
}> = ({
  options,
  value = "",
  onValueChange,
  className,
  children,
  ...restProps
}) => {
  // ...
  return (
    <Popover {...}>
      <PopoverTrigger asChild>
        {children({
          options,
          onValueChange,
          selectedValue,
          setSelectedValue,
          isPopoverOpen,
          setIsPopoverOpen,
          onOptionSelect,
          onClearAllOptions,
        })}
      </PopoverTrigger>
      // ...
    </Popover>
  );
};
InputSelect.displayName = "InputSelect";

export const InputSelectTrigger = React.forwardRef<
  HTMLButtonElement,
  InputSelectProvided & {
    placeholder?: string;
    className?: string;
    children?: (v: SelectOption) => React.ReactNode;
    style?: React.CSSProperties;
  }
>(
  (
    {
      options,
      // onValueChange,
      selectedValue,
      // setSelectedValue,
      // isPopoverOpen,
      setIsPopoverOpen,
      // onOptionSelect,
      onClearAllOptions,
      placeholder = "Select...",
      className,
      style,
      ...restProps,
    },
    ref,
  ) => {
    return (
      <Button
        ref={ref}
        onClick={() => setIsPopoverOpen((prev) => !prev)}
        variant="outline"
        type="button"
        className={cn(
          "flex h-11 w-full items-center justify-between p-1 [&_svg]:pointer-events-auto",
          className,
        )}
        style={style}
        {...restProps}
      >
        {selectedValue ? (
          <div className="flex items-center justify-between w-full">
            <div className="flex items-center px-3 text-foreground">
              {option?.label}
            </div>
            <div className="flex items-center justify-between">
              {selectedValue && clearable && (
                <>
                  <X
                    className={cn(
                      "mx-1 h-4 cursor-pointer text-muted-foreground",
                    )}
                    onClick={(e) => {
                      e.stopPropagation();
                      onClearAllOptions();
                    }}
                  />
                  <Separator orientation="vertical" className="flex h-full min-h-6" />
                </>
              )}
              <ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
            </div>
          </div>
        ) : (
          <div className="flex items-center justify-between w-full mx-auto">
            <span className="px-3 text-sm text-muted-foreground">{placeholder}</span>
            <ChevronDown className="h-4 mx-1 cursor-pointer text-muted-foreground" />
          </div>
        )}
      </Button>
    );
  },
);
InputSelectTrigger.displayName = "InputSelectTrigger";

Now, I can also pass additional props to the <InputSelectTrigger> when I need it.

import * as React from "react";
import { InputSelect, InputSelectTrigger } from "path/to/input-select";

export const MyAwesomeComponent = () => {
  const [value, setValue] = React.useState("apple");

  const options = [...];

  return (
    <div>
      <InputSelect
        options={options}
        value={value}
        onValueChange{(v) => setValue(v)}
      >
        {(provided) => <InputSelectTrigger {...provided} className="additional-styling" />}
      </InputSelect>
    </div>
  );
}

Links

Demo: https://shadcn-components-extend.vercel.app/?component=input-select

Code: https://github.com/Vic-ong/shadcn-components-extend/blob/main/src/components/extend/input-select.tsx


This content originally appeared on DEV Community and was authored by Vic Ong


Print Share Comment Cite Upload Translate Updates
APA

Vic Ong | Sciencx (2025-02-16T22:47:31+00:00) Select Dropdown + Searchbar + Clearable (React & Shadcn). Retrieved from https://www.scien.cx/2025/02/16/select-dropdown-searchbar-clearable-react-shadcn/

MLA
" » Select Dropdown + Searchbar + Clearable (React & Shadcn)." Vic Ong | Sciencx - Sunday February 16, 2025, https://www.scien.cx/2025/02/16/select-dropdown-searchbar-clearable-react-shadcn/
HARVARD
Vic Ong | Sciencx Sunday February 16, 2025 » Select Dropdown + Searchbar + Clearable (React & Shadcn)., viewed ,<https://www.scien.cx/2025/02/16/select-dropdown-searchbar-clearable-react-shadcn/>
VANCOUVER
Vic Ong | Sciencx - » Select Dropdown + Searchbar + Clearable (React & Shadcn). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/02/16/select-dropdown-searchbar-clearable-react-shadcn/
CHICAGO
" » Select Dropdown + Searchbar + Clearable (React & Shadcn)." Vic Ong | Sciencx - Accessed . https://www.scien.cx/2025/02/16/select-dropdown-searchbar-clearable-react-shadcn/
IEEE
" » Select Dropdown + Searchbar + Clearable (React & Shadcn)." Vic Ong | Sciencx [Online]. Available: https://www.scien.cx/2025/02/16/select-dropdown-searchbar-clearable-react-shadcn/. [Accessed: ]
rf:citation
» Select Dropdown + Searchbar + Clearable (React & Shadcn) | Vic Ong | Sciencx | https://www.scien.cx/2025/02/16/select-dropdown-searchbar-clearable-react-shadcn/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.