Custom Auto-Complete Component with Dark Mode Support using Shadcn UI

This page showcases a custom auto-complete component built using the Shadcn UI library for React. The component supports both light and dark modes, offering a sleek and responsive search experience. It’s designed for easy integration, with customizable options like placeholder text, loading states, and empty messages.

Current value: No value selectedLoading state: falseDisabled: false

Integrate the Component

Step1.

Create a file src/somponents/ui/autocomplete.tsx

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 import { CommandGroup, CommandItem, CommandList, CommandInput, } from "@/components/ui/command"; import { Command as CommandPrimitive } from "cmdk"; import { useState, useRef, useCallback, type KeyboardEvent } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { Check } from "lucide-react"; import { cn } from "@/lib/utils"; export type Option = Record<"value" | "label", string> & Record<string, string>; type AutoCompleteProps = { options: Option[]; emptyMessage: string; value?: Option; onValueChange?: (value: Option) => void; isLoading?: boolean; disabled?: boolean; placeholder?: string; className?: string; }; export const AutoComplete = ({ options, placeholder, emptyMessage, value, onValueChange, disabled, isLoading = false, className }: AutoCompleteProps) => { const inputRef = useRef<HTMLInputElement>(null); const [isOpen, setOpen] = useState(false); const [selected, setSelected] = useState<Option>(value as Option); const [inputValue, setInputValue] = useState<string>(value?.label || ""); const handleKeyDown = useCallback( (event: KeyboardEvent<HTMLDivElement>) => { const input = inputRef.current; if (!input) { return; } if (!isOpen) { setOpen(true); } if (event.key === "Enter" && input.value !== "") { const optionToSelect = options.find( (option) => option.label === input.value ); if (optionToSelect) { setSelected(optionToSelect); onValueChange?.(optionToSelect); } } if (event.key === "Escape") { input.blur(); } }, [isOpen, options, onValueChange] ); const handleBlur = useCallback(() => { setOpen(false); setInputValue(selected?.label); }, [selected]); const handleSelectOption = useCallback( (selectedOption: Option) => { setInputValue(selectedOption.label); setSelected(selectedOption); onValueChange?.(selectedOption); setTimeout(() => { inputRef?.current?.blur(); }, 0); }, [onValueChange] ); return ( <CommandPrimitive onKeyDown={handleKeyDown}> <div className={`border rounded-md ${className}`}> <CommandInput ref={inputRef} value={inputValue} onValueChange={isLoading ? undefined : setInputValue} onBlur={handleBlur} onFocus={() => setOpen(true)} placeholder={placeholder} disabled={disabled} /> </div> <div className="relative mt-1"> <div className={cn( "animate-in fade-in-0 zoom-in-95 absolute top-0 z-10 w-full rounded-xl bg-white dark:bg-gray-800 outline-none", isOpen ? "block" : "hidden",className )} > <CommandList className="rounded-lg ring-1 ring-slate-200 dark:ring-gray-700"> {isLoading ? ( <CommandPrimitive.Loading> <div className="p-1"> <Skeleton className="h-8 w-full" /> </div> </CommandPrimitive.Loading> ) : null} {options.length > 0 && !isLoading ? ( <CommandGroup> {options.map((option) => { const isSelected = selected?.value === option.value; return ( <CommandItem key={option.value} value={option.label} onMouseDown={(event) => { event.preventDefault(); event.stopPropagation(); }} onSelect={() => handleSelectOption(option)} className={cn( "flex w-full items-center gap-2 dark:text-white", !isSelected ? "pl-8" : null )} > {isSelected ? <Check className="w-4" /> : null} {option.label} </CommandItem> ); })} </CommandGroup> ) : null} {!isLoading ? ( <CommandPrimitive.Empty className="select-none rounded-sm px-2 py-3 text-center text-sm dark:text-gray-400"> {emptyMessage} </CommandPrimitive.Empty> ) : null} </CommandList> </div> </div> </CommandPrimitive> ); };

Step2.

Import and use

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 "use client"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { AutoComplete, Option } from "@/components/ui/autocomplete"; export default function Page() { const [isLoading, setLoading] = React.useState(false); const [isDisabled, setDisbled] = React.useState(false); const [value, setValue] = React.useState<Option>(); const FRAMEWORKS = [ { 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", }, { value: "nest.js", label: "Nest.js", }, ]; return ( <> <div className="not-prose mt-8 flex flex-col gap-4"> <div className="flex items-center gap-2"> <Button variant={"outline"} onClick={() => setLoading((prev) => !prev)}> Toggle loading </Button> <Button variant={"outline"} onClick={() => setDisbled((prev) => !prev)}> Toggle disabled </Button> </div> <AutoComplete options={FRAMEWORKS} emptyMessage="No resulsts." placeholder="Find something" isLoading={isLoading} onValueChange={setValue} value={value} disabled={isDisabled} /> <span className="text-sm">Current value: {value ? value?.label : "No value selected"}</span> <span className="text-sm">Loading state: {isLoading ? "true" : "false"}</span> <span className="text-sm">Disabled: {isDisabled ? "true" : "false"}</span> </div> </> ); }