Input
Displays a form input field or a component that looks like an input field.
Input text field
The input component can be used to create a text input field. The following example shows a basic text input field.
Input with email validation
The input component can be used to create an email input field. The following example shows an email input field.
Input with password validation
The input component can be used to create a password input field. The following example shows a password input field.
Input with file upload
The input component can be used to create a file upload input field. The following example shows a file upload input field.
Input Counter
The input component can be used to create a number input field. The following example shows a number input field with a counter.
Installation
Create following files to use Input component
Label.tsx
1type LabelProps = {
2 text: string;
3 htmlFor?: string;
4 className?: string;
5};
6
7export const Label = ({ text, htmlFor, className }: LabelProps) => {
8 return (
9 <label
10 htmlFor={htmlFor}
11 className={"block font-geist text-sm font-normal text-rose-100 " + (className || "")}
12 >
13 {text}
14 </label>
15 );
16};
17
HelperText.tsx
1type HelperTextProps = {
2 text: string;
3 variant?: "default" | "error";
4 className?: string;
5};
6
7export const HelperText = ({
8 text,
9 variant = "default",
10 className,
11}: HelperTextProps) => {
12 const variantClass = variant === "error" ? "text-red-500" : "text-rose-200";
13 const combinedClass =
14 "mt-2 text-sm " + variantClass + " " + (className || "");
15 return <p className={combinedClass}>{text}</p>;
16};
17
Input.tsx
1import React, { useState } from "react";
2import { cn } from "@/utils/cn";
3import { Label } from "./Label";
4import { HelperText } from "./HelperText";
5import {
6 X,
7 AlertCircle,
8 Eye,
9 EyeOff,
10 ChevronUp,
11 ChevronDown,
12 Paperclip,
13} from "lucide-react";
14import { ClassNameValue } from "tailwind-merge";
15
16// !! button for file upload
17
18type ButtonProps = {
19 children?: React.ReactNode;
20 className?: ClassNameValue;
21} & React.ButtonHTMLAttributes<HTMLButtonElement>;
22
23const Button = ({ children, className, ...props }: ButtonProps) => {
24 return (
25 <button
26 className={cn(
27 "flex items-center gap-3 rounded-lg bg-rose-500 px-6 py-2 font-geist font-semibold text-rose-100 shadow-md duration-200 ease-in-out hover:shadow-lg hover:brightness-95",
28 className
29 )}
30 {...props}
31 >
32 {children}
33 </button>
34 );
35};
36
37// !! button for file upload
38
39type InputVariants =
40 | "default"
41 | "focused"
42 | "filled"
43 | "disabled"
44 | "error"
45 | "";
46
47type InputProps = {
48 label?: string;
49 helperText?: string;
50 placeholder?: string;
51 className?: string;
52 variant?: InputVariants;
53 type?: "text" | "email" | "password" | "file" | "number";
54 showIcon?: boolean;
55 min?: number;
56 max?: number;
57 step?: number;
58} & React.InputHTMLAttributes<HTMLInputElement>;
59
60export const Input = ({
61 label,
62 helperText,
63 placeholder,
64 className,
65 variant = "",
66 type = "text",
67 showIcon = false,
68 min = 0,
69 max = 100,
70 step = 1,
71 ...props
72}: InputProps) => {
73 const [inputValue, setInputValue] = useState(
74 type === "number" ? props.value || 0 : ""
75 );
76 const [showPassword, setShowPassword] = useState(false);
77 const [passwordStrength, setPasswordStrength] = useState<number | null>(null);
78 const [fileName, setFileName] = useState<string | null>(null);
79
80 const togglePasswordVisibility = () => setShowPassword((prev) => !prev);
81
82 const evaluatePassword = (password: string) => {
83 if (!password) return setPasswordStrength(null);
84 if (password.length < 6) return setPasswordStrength(1);
85 if (/^(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&]).{8,}$/.test(password))
86 return setPasswordStrength(4);
87 if (/^(?=.*[A-Z])(?=.*d).{6,}$/.test(password))
88 return setPasswordStrength(3);
89 return setPasswordStrength(2);
90 };
91
92 const getStrengthLabel = (strength: number | null) => {
93 switch (strength) {
94 case 1:
95 return "Weak";
96 case 2:
97 return "Moderate";
98 case 3:
99 return "Strong";
100 case 4:
101 return "Very Strong";
102 default:
103 return "";
104 }
105 };
106
107 const handleClear = () => setInputValue(type === "number" ? 0 : "");
108 const handleAlert = () => alert("Error: Please check your input.");
109
110 const handleIncrement = () => {
111 setInputValue((prev) => {
112 const newValue = Number(prev) + step;
113 return newValue <= max ? newValue : prev;
114 });
115 };
116
117 const handleDecrement = () => {
118 setInputValue((prev) => {
119 const newValue = Number(prev) - step;
120 return newValue >= min ? newValue : prev;
121 });
122 };
123
124 const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
125 const file = event.target.files?.[0];
126 if (file) {
127 setFileName(file.name);
128 } else {
129 setFileName(null);
130 }
131 };
132
133 const getIcon = () => {
134 if (!showIcon) return null;
135 if (type === "password") {
136 return showPassword ? (
137 <EyeOff
138 onClick={togglePasswordVisibility}
139 className="absolute inset-y-4 right-0 flex items-center pr-3 text-white"
140 width={45}
141 />
142 ) : (
143 <Eye
144 width={45}
145 onClick={togglePasswordVisibility}
146 className="absolute inset-y-4 right-0 flex items-center pr-3 text-white"
147 />
148 );
149 }
150 return getIconForVariant(variant, handleClear, handleAlert);
151 };
152
153 const inputVariantClass = switchInputVariant(variant);
154 const iconClass = showIcon ? "pr-10" : "";
155
156 return (
157 <div className="relative space-y-1 font-geist font-normal text-zinc-100">
158 {label && (
159 <Label
160 text={label}
161 htmlFor={type === "file" ? "file-input" : undefined}
162 />
163 )}
164 <div className="relative flex flex-col items-start">
165 {type === "file" && (
166 <>
167 <input
168 type="file"
169 id="file-input"
170 className="hidden"
171 onChange={handleFileChange}
172 {...props}
173 />
174 <div className="flex min-h-14 min-w-80 items-center rounded-lg bg-transparent px-3 py-2 font-geist text-base font-normal text-zinc-100 outline outline-1 outline-gray-100/10 selection:bg-gray-400/40 placeholder:text-zinc-400 focus:outline focus:outline-gray-100/80 space-x-2">
175 <label htmlFor="file-input" className="cursor-pointer">
176 <Paperclip className="text-white" />
177 </label>
178 <span className="flex-1 text-zinc-400">
179 {fileName || "Add a file"}
180 </span>
181 <Button
182 className="p-2 bg-white text-black rounded-lg"
183 onClick={() => document.getElementById("file-input")?.click()}
184 >
185 Upload
186 </Button>
187 </div>
188 </>
189 )}
190 {type !== "file" && (
191 <>
192 {showIcon && (
193 <span className="absolute inset-y-0 z-10 right-0 flex cursor-pointer items-center justify-center pr-3 mb-9">
194 {getIcon()}
195 </span>
196 )}
197 <div
198 style={{
199 minHeight: "6rem" /* Adjust height as needed */,
200 position: "relative",
201 }}
202 >
203 <input
204 type={type === "password" && showPassword ? "text" : type}
205 placeholder={placeholder}
206 value={inputValue}
207 onChange={(e) => {
208 setInputValue(e.target.value);
209 if (type === "password") evaluatePassword(e.target.value);
210 }}
211 disabled={variant === "disabled"}
212 pattern={type === "email" ? ".*@.*" : undefined}
213 className={cn(
214 "min-h-14 min-w-80 appearance-none rounded-lg bg-transparent px-3 py-2 font-geist text-base font-normal text-zinc-100 outline outline-1 outline-gray-100/10 selection:bg-gray-400/40 placeholder:text-zinc-400 focus:outline focus:outline-gray-100/80",
215 className,
216 iconClass,
217 inputVariantClass
218 )}
219 {...props}
220 />
221 {type === "number" && (
222 <div className="absolute inset-y-0 right-0 flex flex-col items-center justify-center pr-3 mb-9">
223 <ChevronUp
224 className="cursor-pointer"
225 onClick={handleIncrement}
226 />
227 <ChevronDown
228 className="cursor-pointer"
229 onClick={handleDecrement}
230 />
231 </div>
232 )}
233 {type === "password" && (
234 <>
235 <div className="mt-2 flex w-full">
236 <div
237 className={
238 "h-1 flex-1 " +
239 (passwordStrength !== null && passwordStrength >= 1
240 ? "bg-red-500"
241 : "bg-transparent")
242 }
243 ></div>
244 <div
245 className={
246 "h-1 flex-1 " +
247 (passwordStrength !== null && passwordStrength >= 2
248 ? "bg-yellow-500"
249 : "bg-transparent")
250 }
251 ></div>
252 <div
253 className={
254 "h-1 flex-1 " +
255 (passwordStrength !== null && passwordStrength >= 3
256 ? "bg-amber-600"
257 : "bg-transparent")
258 }
259 ></div>
260 <div
261 className={
262 "h-1 flex-1 " +
263 (passwordStrength !== null && passwordStrength >= 4
264 ? "bg-green-500"
265 : "bg-transparent")
266 }
267 ></div>
268 </div>
269 <div className="mt-1 text-sm text-red-100">
270 {getStrengthLabel(passwordStrength)}
271 </div>
272 </>
273 )}
274 </div>
275 </>
276 )}
277 </div>
278 {helperText && <HelperText text={helperText} />}
279 </div>
280 );
281};
282
283const getIconForVariant = (
284 variant: InputVariants,
285 handleClear: () => void,
286 handleAlert: () => void
287) => {
288 switch (variant) {
289 case "focused":
290 case "filled":
291 return <X className="text-white" onClick={handleClear} />;
292 case "disabled":
293 return <AlertCircle className="text-white" onClick={handleAlert} />;
294 case "error":
295 return <AlertCircle className="text-white" onClick={handleAlert} />;
296 default:
297 return null;
298 }
299};
300
301const switchInputVariant = (variant: InputVariants) => {
302 switch (variant) {
303 case "focused":
304 return "focus:outline focus:outline-rose-500";
305 case "filled":
306 return "bg-gray-700/50 focus:bg-transparent";
307 case "disabled":
308 return "bg-gray-500/40 cursor-not-allowed";
309 case "error":
310 return "border-rose-500 focus:outline focus:outline-rose-500";
311 default:
312 return "";
313 }
314};
315
Props
The input component accepts the following props:
Prop | Type | Default | Description |
---|---|---|---|
label | string | undefined | The label for the input field. |
helperText | string | undefined | Helper text to display below the input field. |
placeholder | string | undefined | Placeholder text for the input field. |
className | string | undefined | Additional CSS classes to apply to the input field. |
variant | default , focused , filled , | "" | The visual variant of the input |
disabled , error | |||
type | text , email , password , | text | The type of input field. |
number , file | |||
showIcon | boolean | false | Whether to show an icon in the input field. |
min | number | 0 | The minimum value for a number input field. |
max | number | 100 | The maximum value for a number input field. |
step | number | 1 | The step value for a number input field. |