FluxUI Pro is live - modern UI, powerful animations, zero hassle.
Components
Select

Select

Ultra-modern select component with single/multi-select, searchable options, custom animations, and 3D effects for enhanced user experience.

Select Types

Simple Select

Select a fruit

Selected: None

Multi-Select

Select fruits

Selected: None

Searchable Select

Type to search countries...

Selected: None

With Error State

Please select an option

This field is required

Select Variants

Default Variant

Default style

Filled Variant

Filled style

Outlined Variant

Outlined style

Select Sizes

Small size
Medium size (default)
Large size

Advanced Features

Disabled State

This select is disabled

Large Dataset with Search

Search from many options...

Interactive Form Demo

User Registration Form

Select your country
Choose your favorite fruits
Select size

Keyboard Navigation

Try these keyboard shortcuts:

  • Enter/Space: Open dropdown
  • Arrow Up/Down: Navigate options
  • Enter: Select highlighted option
  • Escape: Close dropdown
  • Click outside: Close dropdown
Use keyboard to navigate

Installation

1

Install the packages

npm i motion clsx tailwind-merge lucide-react
2

Add util file

lib/util.ts
import { ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
3

Copy and paste the following code into your project

select.tsx
"use client"; import { motion, AnimatePresence } from "motion/react"; import React, { useState, useRef, useEffect, useId } from "react"; import { cn } from "@/lib/utils"; import { ChevronDown, Check, X, Search } from "lucide-react"; interface Option { value: string; label: string; disabled?: boolean; } interface SelectProps { options: Option[]; value?: string | string[]; onChange?: (value: string | string[]) => void; multiple?: boolean; searchable?: boolean; placeholder?: string; label?: string; error?: string; disabled?: boolean; size?: "sm" | "md" | "lg"; variant?: "default" | "filled" | "outlined"; className?: string; } const Select: React.FC<SelectProps> = ({ options, value, onChange, multiple = false, searchable = false, placeholder = "Select an option", label, error, disabled = false, size = "md", variant = "default", className, }) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [highlightedIndex, setHighlightedIndex] = useState(-1); const selectRef = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLInputElement>(null); const selectId = useId(); const selectedValues = Array.isArray(value) ? value : value ? [value] : []; const hasError = !!error; // Filter options based on search term const filteredOptions = searchable ? options.filter(option => option.label.toLowerCase().includes(searchTerm.toLowerCase()) ) : options; const baseClasses = "relative w-full transition-all duration-200"; const triggerClasses = { default: "border border-gray-300 bg-white focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:focus:border-blue-400", filled: "border-transparent bg-gray-100 focus:bg-white focus:border-blue-500 focus:ring-blue-500/20 dark:bg-gray-800 dark:focus:bg-gray-900", outlined: "border-2 border-gray-300 bg-transparent focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-600", }; const sizeClasses = { sm: "px-3 py-2 text-sm", md: "px-4 py-3 text-base", lg: "px-4 py-4 text-lg", }; const errorClasses = "border-red-500 focus:border-red-500 focus:ring-red-500/20"; // Get display text for selected values const getDisplayText = () => { if (selectedValues.length === 0) return placeholder; if (multiple) { if (selectedValues.length === 1) { const option = options.find(opt => opt.value === selectedValues[0]); return option?.label || selectedValues[0]; } return `${selectedValues.length} selected`; } const option = options.find(opt => opt.value === selectedValues[0]); return option?.label || selectedValues[0]; }; // Handle option selection const handleOptionSelect = (optionValue: string) => { if (multiple) { const newValues = selectedValues.includes(optionValue) ? selectedValues.filter(v => v !== optionValue) : [...selectedValues, optionValue]; onChange?.(newValues); } else { onChange?.(optionValue); setIsOpen(false); } setSearchTerm(""); setHighlightedIndex(-1); }; // Handle keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => { if (!isOpen) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setIsOpen(true); } return; } switch (e.key) { case "ArrowDown": e.preventDefault(); setHighlightedIndex(prev => prev < filteredOptions.length - 1 ? prev + 1 : 0 ); break; case "ArrowUp": e.preventDefault(); setHighlightedIndex(prev => prev > 0 ? prev - 1 : filteredOptions.length - 1 ); break; case "Enter": e.preventDefault(); if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) { handleOptionSelect(filteredOptions[highlightedIndex].value); } break; case "Escape": e.preventDefault(); setIsOpen(false); setHighlightedIndex(-1); break; } }; // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (selectRef.current && !selectRef.current.contains(event.target as Node)) { setIsOpen(false); setHighlightedIndex(-1); setSearchTerm(""); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Focus management useEffect(() => { if (isOpen && searchable && inputRef.current) { inputRef.current.focus(); } }, [isOpen, searchable]); return ( <div className={cn(baseClasses, className)}> {/* Label */} {label && ( <motion.label htmlFor={selectId} className={cn( "block text-sm font-medium mb-2 transition-colors duration-200", hasError ? "text-red-600 dark:text-red-400" : "text-gray-700 dark:text-gray-300" )} animate={{ scale: isOpen || selectedValues.length > 0 ? 1 : 1, y: isOpen || selectedValues.length > 0 ? 0 : 0, }} transition={{ duration: 0.2 }} > {label} </motion.label> )} {/* Select Trigger */} <div ref={selectRef}> <motion.div className={cn( "relative flex items-center justify-between cursor-pointer rounded-lg transition-all duration-200", triggerClasses[variant], sizeClasses[size], hasError && errorClasses, disabled && "opacity-60 cursor-not-allowed bg-gray-50 dark:bg-gray-900", isOpen && "ring-2 ring-blue-500/20" )} onClick={() => !disabled && setIsOpen(!isOpen)} onKeyDown={handleKeyDown} tabIndex={disabled ? -1 : 0} role="combobox" aria-expanded={isOpen} aria-haspopup="listbox" aria-labelledby={label ? selectId : undefined} animate={{ scale: isOpen ? 1.01 : 1, boxShadow: isOpen ? "0 4px 12px rgba(0, 0, 0, 0.1)" : "0 1px 3px rgba(0, 0, 0, 0.1)", }} transition={{ duration: 0.2 }} > {/* Selected Value Display */} <span className={cn( "flex-1 truncate", selectedValues.length === 0 && "text-gray-500 dark:text-gray-400" )}> {getDisplayText()} </span> {/* Multi-select tags */} {multiple && selectedValues.length > 1 && ( <div className="flex flex-wrap gap-1 mr-2"> {selectedValues.slice(0, 2).map(val => { const option = options.find(opt => opt.value === val); return ( <motion.span key={val} className="inline-flex items-center px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-md dark:bg-blue-900 dark:text-blue-200" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} > {option?.label || val} <button onClick={(e) => { e.stopPropagation(); handleOptionSelect(val); }} className="ml-1 hover:bg-blue-200 rounded dark:hover:bg-blue-800" > <X className="w-3 h-3" /> </button> </motion.span> ); })} {selectedValues.length > 2 && ( <span className="text-xs text-gray-500"> +{selectedValues.length - 2} more </span> )} </div> )} {/* Dropdown Arrow */} <motion.div animate={{ rotate: isOpen ? 180 : 0 }} transition={{ duration: 0.2 }} className="ml-2" > <ChevronDown className="w-4 h-4 text-gray-400" /> </motion.div> </motion.div> {/* Dropdown Menu */} <AnimatePresence> {isOpen && ( <motion.div className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700 max-h-60 overflow-hidden" initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -10, scale: 0.95 }} transition={{ duration: 0.2 }} > {/* Search Input */} {searchable && ( <div className="p-2 border-b border-gray-200 dark:border-gray-700"> <div className="relative"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" /> <input ref={inputRef} type="text" placeholder="Search options..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:bg-gray-700 dark:border-gray-600 dark:text-white" onKeyDown={(e) => { e.stopPropagation(); if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter") { handleKeyDown(e); } }} /> </div> </div> )} {/* Options List */} <div className="max-h-48 overflow-y-auto"> {filteredOptions.length === 0 ? ( <div className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"> No options found </div> ) : ( filteredOptions.map((option, index) => { const isSelected = selectedValues.includes(option.value); const isHighlighted = index === highlightedIndex; return ( <motion.div key={option.value} className={cn( "px-4 py-3 cursor-pointer transition-colors duration-150 flex items-center justify-between", option.disabled && "opacity-50 cursor-not-allowed", isSelected && "bg-blue-50 text-blue-700 dark:bg-blue-900 dark:text-blue-200", isHighlighted && !isSelected && "bg-gray-100 dark:bg-gray-700", !option.disabled && !isSelected && "hover:bg-gray-50 dark:hover:bg-gray-700" )} onClick={() => !option.disabled && handleOptionSelect(option.value)} animate={{ backgroundColor: isHighlighted && !isSelected ? "rgba(0, 0, 0, 0.05)" : isSelected ? "rgba(59, 130, 246, 0.1)" : "transparent", }} transition={{ duration: 0.15 }} > <span className="flex-1">{option.label}</span> {isSelected && ( <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ duration: 0.2 }} > <Check className="w-4 h-4 text-blue-600 dark:text-blue-400" /> </motion.div> )} </motion.div> ); }) )} </div> </motion.div> )} </AnimatePresence> </div> {/* Error Message */} <AnimatePresence> {hasError && ( <motion.div initial={{ opacity: 0, y: -10, height: 0 }} animate={{ opacity: 1, y: 0, height: "auto" }} exit={{ opacity: 0, y: -10, height: 0 }} transition={{ duration: 0.2 }} className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1" > <X className="w-4 h-4 flex-shrink-0" /> {error} </motion.div> )} </AnimatePresence> </div> ); }; export default Select;
4

Update the import paths to match your project setup

Props

PropTypeDefaultDescription
optionsArray<{value: string, label: string, disabled?: boolean}>[]Array of options to display in the dropdown.
valuestring | string[]undefinedSelected value(s) for controlled component.
multiplebooleanfalseEnable multi-select mode.
searchablebooleanfalseEnable search functionality.
placeholderstring'Select an option'Placeholder text when no option is selected.
labelstringundefinedLabel text for the select.
errorstringundefinedError message to display.
disabledbooleanfalseDisable the select component.
size'sm' | 'md' | 'lg''md'Size of the select component.
variant'default' | 'filled' | 'outlined''default'Visual style variant.