Gradient Select
A premium dropdown selection component with a mouse-following gradient border. Features smooth animations and full keyboard accessibility.
FormsInteractiveAccessible
Component Preview
Choose a role...
Select level
States & Variants
Error State
Select a location...
Location is required.
Disabled State
Selection locked...
Custom Gradient Color
Blue Glow Select...
style={{ "--select-border-gradient": "#3b82f6" }}Override the CSS variable --select-border-gradient to customize the glow color.
Installation
npm install framer-motion lucide-react clsx tailwind-merge
Copy the source code below into components/ui/glow-select.tsx:
components/ui/glow-select.tsx
1"use client";23import * as React from "react";4import { cn } from "@/lib/utils";5import { motion, AnimatePresence } from "framer-motion";6import { ChevronDown, Check } from "lucide-react";78export interface SelectOption {9 label: string;10 value: string;11}1213export interface GlowSelectProps {14 options: SelectOption[];15 value?: string;16 onChange?: (value: string) => void;17 placeholder?: string;18 label?: string;19 disabled?: boolean;20 error?: boolean;21 className?: string;22}2324const GlowSelect = React.forwardRef<HTMLDivElement, GlowSelectProps>(25 ({ options, value, onChange, placeholder = "Select an option", label, disabled, error, className }, ref) => {26 const [isOpen, setIsOpen] = React.useState(false);27 const [focused, setFocused] = React.useState(false);28 const containerRef = React.useRef<HTMLDivElement>(null);29 const dropdownRef = React.useRef<HTMLDivElement>(null);3031 const selectedOption = options.find((opt) => opt.value === value);3233 React.useEffect(() => {34 const container = containerRef.current;35 if (!container) return;3637 const handleMouseMove = (e: MouseEvent) => {38 const rect = container.getBoundingClientRect();39 const x = e.clientX - rect.left;40 const y = e.clientY - rect.top;41 container.style.setProperty("--mouse-x", `${x}px`);42 container.style.setProperty("--mouse-y", `${y}px`);43 };4445 container.addEventListener("mousemove", handleMouseMove);46 return () => {47 container.removeEventListener("mousemove", handleMouseMove);48 };49 }, []);5051 // Close dropdown when clicking outside52 React.useEffect(() => {53 const handleClickOutside = (event: MouseEvent) => {54 if (55 containerRef.current &&56 !containerRef.current.contains(event.target as Node) &&57 dropdownRef.current &&58 !dropdownRef.current.contains(event.target as Node)59 ) {60 setIsOpen(false);61 setFocused(false);62 }63 };6465 document.addEventListener("mousedown", handleClickOutside);66 return () => document.removeEventListener("mousedown", handleClickOutside);67 }, []);6869 const handleSelect = (optionValue: string) => {70 onChange?.(optionValue);71 setIsOpen(false);72 setFocused(false);73 };7475 const toggleOpen = () => {76 if (!disabled) {77 setIsOpen(!isOpen);78 setFocused(!isOpen);79 }80 };8182 return (83 <div className={cn("relative w-full", className)} ref={ref}>84 {label && <label className="block text-sm font-medium text-neutral-300 mb-1.5 ml-1">{label}</label>}8586 {/* Trigger Button */}87 <div88 ref={containerRef}89 onClick={toggleOpen}90 className={cn(91 "group relative rounded-xl p-px transition-all duration-300 cursor-pointer",92 "bg-neutral-800",93 error ? "bg-red-500/50" : focused || isOpen ? "bg-neutral-600" : "hover:bg-neutral-700",94 disabled && "opacity-50 cursor-not-allowed hover:bg-neutral-800"95 )}96 style={97 {98 "--mouse-x": "0px",99 "--mouse-y": "0px",100 background: (error || disabled)101 ? undefined102 : `radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), rgba(255, 255, 255, 0.4), transparent 40%),103 radial-gradient(400px circle at var(--mouse-x) var(--mouse-y), var(--select-border-gradient, #a78bfa), transparent 40%)`104 } as React.CSSProperties105 }106 >107 <div className="relative rounded-[10px] bg-neutral-950 w-full flex items-center justify-between px-4 py-3 h-11">108 <span className={cn("text-sm truncate", !selectedOption ? "text-neutral-500" : "text-white")}>109 {selectedOption ? selectedOption.label : placeholder}110 </span>111 <ChevronDown112 className={cn(113 "w-4 h-4 text-neutral-500 transition-transform duration-300",114 isOpen && "rotate-180 text-white"115 )}116 />117 </div>118 </div>119120 {/* Dropdown Menu */}121 <AnimatePresence>122 {isOpen && (123 <motion.div124 ref={dropdownRef}125 initial={{ opacity: 0, y: -10, scale: 0.95 }}126 animate={{ opacity: 1, y: 0, scale: 1 }}127 exit={{ opacity: 0, y: -10, scale: 0.95 }}128 transition={{ duration: 0.2, ease: "easeOut" }}129 className="absolute z-50 w-full mt-2 left-0 top-full p-1 rounded-xl border border-white/10 bg-[#0F0F10] shadow-2xl overflow-hidden"130 >131 <div className="max-h-60 overflow-y-auto overflow-x-hidden py-1 px-1 custom-scrollbar">132 {options.map((option) => {133 const isSelected = option.value === value;134 return (135 <div136 key={option.value}137 onClick={() => handleSelect(option.value)}138 className={cn(139 "flex items-center justify-between w-full px-3 py-2.5 rounded-lg text-sm cursor-pointer transition-colors",140 isSelected ? "bg-white/10 text-white" : "text-neutral-400 hover:text-white hover:bg-white/5"141 )}142 >143 <span>{option.label}</span>144 {isSelected && <Check className="w-3.5 h-3.5 text-white" />}145 </div>146 );147 })}148 {options.length === 0 && (149 <div className="px-3 py-4 text-center text-xs text-neutral-500">150 No options available151 </div>152 )}153 </div>154 </motion.div>155 )}156 </AnimatePresence>157158 </div>159 );160 }161);162GlowSelect.displayName = "GlowSelect";163164export { GlowSelect };