Falnix UI

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:

1"use client";
2
3import * as React from "react";
4import { cn } from "@/lib/utils";
5import { motion, AnimatePresence } from "framer-motion";
6import { ChevronDown, Check } from "lucide-react";
7
8export interface SelectOption {
9 label: string;
10 value: string;
11}
12
13export 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}
23
24const 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);
30
31 const selectedOption = options.find((opt) => opt.value === value);
32
33 React.useEffect(() => {
34 const container = containerRef.current;
35 if (!container) return;
36
37 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 };
44
45 container.addEventListener("mousemove", handleMouseMove);
46 return () => {
47 container.removeEventListener("mousemove", handleMouseMove);
48 };
49 }, []);
50
51 // Close dropdown when clicking outside
52 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 };
64
65 document.addEventListener("mousedown", handleClickOutside);
66 return () => document.removeEventListener("mousedown", handleClickOutside);
67 }, []);
68
69 const handleSelect = (optionValue: string) => {
70 onChange?.(optionValue);
71 setIsOpen(false);
72 setFocused(false);
73 };
74
75 const toggleOpen = () => {
76 if (!disabled) {
77 setIsOpen(!isOpen);
78 setFocused(!isOpen);
79 }
80 };
81
82 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>}
85
86 {/* Trigger Button */}
87 <div
88 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 ? undefined
102 : `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.CSSProperties
105 }
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 <ChevronDown
112 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>
119
120 {/* Dropdown Menu */}
121 <AnimatePresence>
122 {isOpen && (
123 <motion.div
124 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 <div
136 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 available
151 </div>
152 )}
153 </div>
154 </motion.div>
155 )}
156 </AnimatePresence>
157
158 </div>
159 );
160 }
161);
162GlowSelect.displayName = "GlowSelect";
163
164export { GlowSelect };