Note: To use password visibility toggle, install react-icons with npm install react-icons.
1npm install react-icons
1import { Input } from "@/src/components/shared/Input"; 2 3export default function Example() { 4 return ( 5 <div className="max-w-md"> 6 <Input 7 label="Correo electronico" 8 defaultValue="" 9 type="text" 10 variant="outline" 11 size="md" 12 rounded="full" 13 color="#ffffff" 14 textColor="#ffffff" 15 selectedBgColor="#ffffff" 16 selectedTextColor="#000000" 17 duration="300ms" 18 shadowOnFocus={true} 19 /> 20 </div> 21 ); 22}
1"use client"; 2import { useId, useState, type InputHTMLAttributes } from "react"; 3import { FaEye, FaEyeSlash } from "react-icons/fa6"; 4 5type InputProps = Omit< 6 InputHTMLAttributes<HTMLInputElement>, 7 "size" | "className" | "style" 8> & { 9 label: string; 10 color?: string; 11 textColor?: string; 12 selectedTextColor?: string; 13 selectedBgColor?: string; 14 rounded?: "none" | "lg" | "xl" | "full"; 15 duration?: string; 16 variant?: "fill" | "outline"; 17 size?: "sm" | "md" | "lg"; 18 shadowOnFocus?: boolean; 19 20 labelClassName?: string; 21 inputClassName?: string; 22 fatherClassName?: string; 23}; 24 25const hexToRgba = (hex: string, alpha: number) => { 26 const clean = hex.trim().replace(/^#/, ""); 27 const normalized = 28 clean.length === 3 29 ? clean 30 .split("") 31 .map((char) => char + char) 32 .join("") 33 : clean; 34 35 if (!/^[\da-fA-F]{6}$/.test(normalized)) { 36 return `rgba(0, 0, 0, ${Math.min(Math.max(alpha, 0), 1)})`; 37 } 38 39 const r = parseInt(normalized.slice(0, 2), 16); 40 const g = parseInt(normalized.slice(2, 4), 16); 41 const b = parseInt(normalized.slice(4, 6), 16); 42 const safeAlpha = Math.min(Math.max(alpha, 0), 1); 43 44 return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`; 45}; 46 47export const Input: React.FC<InputProps> = ({ 48 label = "Name", 49 type = "text", 50 id: providedId, 51 name: providedName, 52 color = "#ffffff", 53 textColor = "#ffffff", 54 selectedTextColor = "#000000", 55 selectedBgColor = "#ffffff", 56 rounded = "xl", 57 duration = "300ms", 58 variant = "outline", 59 size = "md", 60 shadowOnFocus = true, 61 labelClassName = "", 62 inputClassName = "", 63 fatherClassName = "", 64 onFocus, 65 onBlur, 66 ...inputProps 67}) => { 68 const [showPassword, setShowPassword] = useState(false); 69 const generatedId = useId(); 70 const inputId = providedId ?? generatedId; 71 const inputName = providedName ?? label; 72 const isPasswordType = type === "password"; 73 const resolvedType = isPasswordType && showPassword ? "text" : type; 74 75 const isName = 76 label.toLowerCase() === "name" || label.toLowerCase() === "nombre"; 77 78 const isLastName = 79 label.toLowerCase() === "last name" || label.toLowerCase() === "apellido"; 80 81 const roundedMap = { 82 none: "rounded-none", 83 lg: "rounded-lg", 84 xl: "rounded-xl", 85 full: "rounded-full", 86 }; 87 88 const inputSizeMap = { 89 sm: "text-sm", 90 md: "text-base", 91 lg: "text-lg", 92 }; 93 94 const floatingLabelSizeMap = { 95 sm: "peer-focus:text-xs peer-not-placeholder-shown:text-xs", 96 md: "peer-focus:text-sm peer-not-placeholder-shown:text-sm", 97 lg: "peer-focus:text-base peer-not-placeholder-shown:text-base", 98 }; 99 100 const labelSizeMap = { 101 sm: "text-sm", 102 md: "text-base", 103 lg: "text-lg", 104 }; 105 106 const floatingOffsetMap = { 107 sm: "-0.5rem", 108 md: "-0.625rem", 109 lg: "-0.75rem", 110 }; 111 112 const wrapperPaddingMap = { 113 sm: "has-[input:focus]:pt-4 has-[input:not(:placeholder-shown)]:pt-4", 114 md: "has-[input:focus]:pt-5 has-[input:not(:placeholder-shown)]:pt-5", 115 lg: "has-[input:focus]:pt-6 has-[input:not(:placeholder-shown)]:pt-6", 116 }; 117 118 return ( 119 <div 120 data-input-id={inputId} 121 className={` 122 flex flex-col gap-2 relative 123 transition-all 124 w-full flex-1 125 ${wrapperPaddingMap[size]} 126 ${fatherClassName} 127 `} 128 style={{ transitionDuration: duration }} 129 > 130 <div className="relative w-full"> 131 <input 132 {...inputProps} 133 type={resolvedType} 134 name={inputName} 135 id={inputId} 136 placeholder=" " 137 className={` 138 peer border p-2 px-4 w-full 139 focus:outline-none 140 transition-all 141 placeholder-transparent 142 ${inputSizeMap[size]} 143 ${inputClassName} 144 ${roundedMap[rounded]} 145 ${isPasswordType ? "pr-11" : ""} 146 ${isName || isLastName ? "capitalize" : ""} 147 `} 148 style={{ 149 color: textColor, 150 transitionDuration: duration, 151 }} 152 onFocus={(e) => { 153 onFocus?.(e); 154 if (shadowOnFocus) { 155 e.currentTarget.style.boxShadow = 156 variant === "fill" 157 ? `0px 0px 3px ${color}` 158 : `0px 0px 8px ${color}`; 159 } 160 }} 161 onBlur={(e) => { 162 onBlur?.(e); 163 if (shadowOnFocus) { 164 e.currentTarget.style.boxShadow = "none"; 165 } 166 }} 167 /> 168 169 {isPasswordType ? ( 170 <div className="absolute inset-y-0 right-3 flex items-center"> 171 <button 172 type="button" 173 aria-label={showPassword ? "Hide password" : "Show password"} 174 onClick={() => setShowPassword((prev) => !prev)} 175 className="text-foreground/70 hover:text-foreground transition-colors" 176 > 177 {showPassword ? <FaEyeSlash size={14} /> : <FaEye size={14} />} 178 </button> 179 </div> 180 ) : null} 181 </div> 182 183 <label 184 htmlFor={inputId} 185 className={` 186 absolute inset-y-0 left-4 187 flex items-center 188 transition-all 189 pointer-events-none 190 ${labelSizeMap[size]} 191 ${floatingLabelSizeMap[size]} 192 ${labelClassName} 193 `} 194 style={{ 195 transitionDuration: duration, 196 }} 197 > 198 {label} 199 </label> 200 201 <style>{` 202 [data-input-id="${inputId}"] label { 203 left: 1rem; 204 transition-duration: ${duration}; 205 transition-property: color, opacity, top, bottom, left, transform, font-size; 206 color: ${textColor}; 207 opacity: 0.3; 208 } 209 210 [data-input-id="${inputId}"] input { 211 border-color: ${variant === "outline" ? hexToRgba(color, 0.3) : "transparent"}; 212 background: ${ 213 variant === "fill" ? hexToRgba(color, 0.08) : "transparent" 214 }; 215 } 216 217 [data-input-id="${inputId}"] input:focus { 218 border-color: ${variant === "outline" ? color : "transparent"}; 219 background: ${ 220 variant === "fill" ? hexToRgba(color, 0.16) : "transparent" 221 }; 222 } 223 224 [data-input-id="${inputId}"]:has(input:focus) label { 225 top: ${floatingOffsetMap[size]}; 226 bottom: auto; 227 left: 0; 228 transform: translateY(0); 229 } 230 231 [data-input-id="${inputId}"]:has(input:not(:placeholder-shown)) label { 232 top: ${floatingOffsetMap[size]}; 233 bottom: auto; 234 left: 0; 235 transform: translateY(0); 236 } 237 238 [data-input-id="${inputId}"]:has(input:focus) label, 239 [data-input-id="${inputId}"]:has(input:not(:placeholder-shown)) label { 240 opacity: 1; 241 color: ${color}; 242 } 243 244 [data-input-id="${inputId}"] input::selection { 245 background: ${selectedBgColor}; 246 color: ${selectedTextColor}; 247 } 248 `}</style> 249 </div> 250 ); 251}; 252


