Combobox

Form

Searchable dropdown with autocomplete filtering.

Preview

Usage

example.jsx
import { Combobox } from "@/components/ui/combobox";

export default function Example() {
  return <Combobox />;
}

Source Code

Copy this file into components/ui/combobox.jsx in your project.

combobox.jsx
"use client";

import { forwardRef, useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";

const Combobox = forwardRef(
  ({ className, options = [], value, onValueChange, placeholder = "Search...", ...props }, ref) => {
    const [open, setOpen] = useState(false);
    const [query, setQuery] = useState("");
    const wrapperRef = useRef(null);

    const filtered = options.filter((opt) =>
      opt.label.toLowerCase().includes(query.toLowerCase())
    );

    const selected = options.find((opt) => opt.value === value);

    useEffect(() => {
      const handleClick = (e) => {
        if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
      };
      document.addEventListener("mousedown", handleClick);
      return () => document.removeEventListener("mousedown", handleClick);
    }, []);

    return (
      <div ref={wrapperRef} className={cn("relative", className)} {...props}>
        <input
          ref={ref}
          type="text"
          role="combobox"
          aria-expanded={open}
          value={open ? query : selected?.label || ""}
          placeholder={placeholder}
          onFocus={() => { setOpen(true); setQuery(""); }}
          onChange={(e) => setQuery(e.target.value)}
          className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
        />
        {open && (
          <div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 shadow-md">
            {filtered.length === 0 && (
              <div className="px-2 py-1.5 text-sm text-muted-foreground">No results.</div>
            )}
            {filtered.map((opt) => (
              <button
                key={opt.value}
                type="button"
                onClick={() => { onValueChange?.(opt.value); setOpen(false); setQuery(""); }}
                className={cn(
                  "flex w-full items-center rounded-sm px-2 py-1.5 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground",
                  value === opt.value && "bg-accent text-accent-foreground"
                )}
              >
                {opt.label}
              </button>
            ))}
          </div>
        )}
      </div>
    );
  }
);
Combobox.displayName = "Combobox";

export { Combobox };

Quick Install

Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.

npm install clsx tailwind-merge