Components
Input
Input
Ultra-modern input component with multiple types, validation states, icons, and smooth animations for enhanced user experience.
Basic Input Types
Text Input
Email Input
Password Input
Search Input
Number Input
Phone Input
Input Variants
Default Variant
Filled Variant
Outlined Variant
Underlined Variant
Input Sizes
Input States
Normal State
Error State
Please enter a valid email address
Success State
Interactive Form Demo
Installation
1
Install the packages
npm i motion clsx tailwind-merge lucide-react2
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));
}
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
input.tsx
"use client";
import { motion, AnimatePresence } from "motion/react";
import React, { useState, useId } from "react";
import { cn } from "@/lib/utils";
import { Eye, EyeOff, Check, X } from "lucide-react";
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
variant?: "default" | "filled" | "outlined" | "underlined";
size?: "sm" | "md" | "lg";
label?: string;
error?: string;
success?: boolean;
icon?: React.ReactNode;
iconPosition?: "left" | "right";
showPasswordToggle?: boolean;
}
const Input: React.FC<InputProps> = ({
variant = "default",
size = "md",
label,
error,
success,
icon,
iconPosition = "left",
showPasswordToggle = false,
type = "text",
className,
id,
...props
}) => {
const [showPassword, setShowPassword] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [hasValue, setHasValue] = useState(!!props.value || !!props.defaultValue);
const inputId = id || useId();
const inputType = type === "password" && showPassword ? "text" : type;
const hasError = !!error;
const hasSuccess = success && !hasError;
const baseClasses = "relative w-full transition-all duration-200 bg-transparent border outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50";
const variantClasses = {
default: "border-gray-300 focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-600 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",
underlined: "border-b-2 border-gray-300 bg-transparent rounded-none px-0 focus:border-blue-500 focus:ring-0 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 stateClasses = {
error: "border-red-500 focus:border-red-500 focus:ring-red-500/20",
success: "border-green-500 focus:border-green-500 focus:ring-green-500/20",
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(true);
props.onFocus?.(e);
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(false);
props.onBlur?.(e);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setHasValue(!!e.target.value);
props.onChange?.(e);
};
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
return (
<motion.div
className="relative"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* Label */}
{label && (
<motion.label
htmlFor={inputId}
className={cn(
"block text-sm font-medium mb-2 transition-colors duration-200",
hasError ? "text-red-600 dark:text-red-400" :
hasSuccess ? "text-green-600 dark:text-green-400" :
"text-gray-700 dark:text-gray-300"
)}
animate={{
scale: isFocused || hasValue ? 1 : 1,
y: isFocused || hasValue ? 0 : 0,
}}
transition={{ duration: 0.2 }}
>
{label}
{props.required && <span className="text-red-500 ml-1">*</span>}
</motion.label>
)}
{/* Input Container */}
<div className="relative">
{/* Left Icon */}
{icon && iconPosition === "left" && (
<motion.div
className={cn(
"absolute left-3 top-1/2 transform -translate-y-1/2 z-10",
hasError ? "text-red-500" :
hasSuccess ? "text-green-500" :
"text-gray-400"
)}
animate={{
scale: isFocused ? 1.1 : 1,
x: isFocused ? -2 : 0,
}}
transition={{ duration: 0.2 }}
>
{icon}
</motion.div>
)}
{/* Input Field */}
<motion.input
id={inputId}
type={inputType}
className={cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
hasError && stateClasses.error,
hasSuccess && stateClasses.success,
icon && iconPosition === "left" && "pl-10",
(icon && iconPosition === "right") || showPasswordToggle ? "pr-10" : "",
className
)}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleChange}
animate={{
scale: isFocused ? 1.01 : 1,
boxShadow: isFocused
? hasError
? "0 0 0 3px rgba(239, 68, 68, 0.1)"
: hasSuccess
? "0 0 0 3px rgba(34, 197, 94, 0.1)"
: "0 0 0 3px rgba(59, 130, 246, 0.1)"
: "0 0 0 0px rgba(0, 0, 0, 0)",
}}
transition={{ duration: 0.2 }}
{...props}
/>
{/* Right Icon or Password Toggle */}
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center gap-2 z-10">
{/* Success Icon */}
<AnimatePresence>
{hasSuccess && !isFocused && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Check className="w-4 h-4 text-green-500" />
</motion.div>
)}
</AnimatePresence>
{/* Error Icon */}
<AnimatePresence>
{hasError && !isFocused && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<X className="w-4 h-4 text-red-500" />
</motion.div>
)}
</AnimatePresence>
{/* Password Toggle */}
{showPasswordToggle && type === "password" && (
<motion.button
type="button"
onClick={togglePasswordVisibility}
className={cn(
"p-1 rounded-md transition-colors duration-200",
hasError ? "text-red-500 hover:bg-red-50 dark:hover:bg-red-950" :
hasSuccess ? "text-green-500 hover:bg-green-50 dark:hover:bg-green-950" :
"text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
{showPassword ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</motion.button>
)}
{/* Right Icon */}
{icon && iconPosition === "right" && !showPasswordToggle && (
<motion.div
className={cn(
"z-10",
hasError ? "text-red-500" :
hasSuccess ? "text-green-500" :
"text-gray-400"
)}
animate={{
scale: isFocused ? 1.1 : 1,
x: isFocused ? 2 : 0,
}}
transition={{ duration: 0.2 }}
>
{icon}
</motion.div>
)}
</div>
{/* Focus Ring Animation */}
<motion.div
className="absolute inset-0 rounded-md pointer-events-none"
animate={{
boxShadow: isFocused
? hasError
? "0 0 0 2px rgba(239, 68, 68, 0.2)"
: hasSuccess
? "0 0 0 2px rgba(34, 197, 94, 0.2)"
: "0 0 0 2px rgba(59, 130, 246, 0.2)"
: "0 0 0 0px rgba(0, 0, 0, 0)",
}}
transition={{ duration: 0.2 }}
/>
</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>
{/* Success Message */}
<AnimatePresence>
{hasSuccess && !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-green-600 dark:text-green-400 flex items-center gap-1"
>
<Check className="w-4 h-4 flex-shrink-0" />
Looks good!
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
export default Input;"use client";
import { motion, AnimatePresence } from "motion/react";
import React, { useState, useId } from "react";
import { cn } from "@/lib/utils";
import { Eye, EyeOff, Check, X } from "lucide-react";
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
variant?: "default" | "filled" | "outlined" | "underlined";
size?: "sm" | "md" | "lg";
label?: string;
error?: string;
success?: boolean;
icon?: React.ReactNode;
iconPosition?: "left" | "right";
showPasswordToggle?: boolean;
}
const Input: React.FC<InputProps> = ({
variant = "default",
size = "md",
label,
error,
success,
icon,
iconPosition = "left",
showPasswordToggle = false,
type = "text",
className,
id,
...props
}) => {
const [showPassword, setShowPassword] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [hasValue, setHasValue] = useState(!!props.value || !!props.defaultValue);
const inputId = id || useId();
const inputType = type === "password" && showPassword ? "text" : type;
const hasError = !!error;
const hasSuccess = success && !hasError;
const baseClasses = "relative w-full transition-all duration-200 bg-transparent border outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50";
const variantClasses = {
default: "border-gray-300 focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-600 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",
underlined: "border-b-2 border-gray-300 bg-transparent rounded-none px-0 focus:border-blue-500 focus:ring-0 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 stateClasses = {
error: "border-red-500 focus:border-red-500 focus:ring-red-500/20",
success: "border-green-500 focus:border-green-500 focus:ring-green-500/20",
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(true);
props.onFocus?.(e);
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(false);
props.onBlur?.(e);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setHasValue(!!e.target.value);
props.onChange?.(e);
};
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
return (
<motion.div
className="relative"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* Label */}
{label && (
<motion.label
htmlFor={inputId}
className={cn(
"block text-sm font-medium mb-2 transition-colors duration-200",
hasError ? "text-red-600 dark:text-red-400" :
hasSuccess ? "text-green-600 dark:text-green-400" :
"text-gray-700 dark:text-gray-300"
)}
animate={{
scale: isFocused || hasValue ? 1 : 1,
y: isFocused || hasValue ? 0 : 0,
}}
transition={{ duration: 0.2 }}
>
{label}
{props.required && <span className="text-red-500 ml-1">*</span>}
</motion.label>
)}
{/* Input Container */}
<div className="relative">
{/* Left Icon */}
{icon && iconPosition === "left" && (
<motion.div
className={cn(
"absolute left-3 top-1/2 transform -translate-y-1/2 z-10",
hasError ? "text-red-500" :
hasSuccess ? "text-green-500" :
"text-gray-400"
)}
animate={{
scale: isFocused ? 1.1 : 1,
x: isFocused ? -2 : 0,
}}
transition={{ duration: 0.2 }}
>
{icon}
</motion.div>
)}
{/* Input Field */}
<motion.input
id={inputId}
type={inputType}
className={cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
hasError && stateClasses.error,
hasSuccess && stateClasses.success,
icon && iconPosition === "left" && "pl-10",
(icon && iconPosition === "right") || showPasswordToggle ? "pr-10" : "",
className
)}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleChange}
animate={{
scale: isFocused ? 1.01 : 1,
boxShadow: isFocused
? hasError
? "0 0 0 3px rgba(239, 68, 68, 0.1)"
: hasSuccess
? "0 0 0 3px rgba(34, 197, 94, 0.1)"
: "0 0 0 3px rgba(59, 130, 246, 0.1)"
: "0 0 0 0px rgba(0, 0, 0, 0)",
}}
transition={{ duration: 0.2 }}
{...props}
/>
{/* Right Icon or Password Toggle */}
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center gap-2 z-10">
{/* Success Icon */}
<AnimatePresence>
{hasSuccess && !isFocused && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Check className="w-4 h-4 text-green-500" />
</motion.div>
)}
</AnimatePresence>
{/* Error Icon */}
<AnimatePresence>
{hasError && !isFocused && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<X className="w-4 h-4 text-red-500" />
</motion.div>
)}
</AnimatePresence>
{/* Password Toggle */}
{showPasswordToggle && type === "password" && (
<motion.button
type="button"
onClick={togglePasswordVisibility}
className={cn(
"p-1 rounded-md transition-colors duration-200",
hasError ? "text-red-500 hover:bg-red-50 dark:hover:bg-red-950" :
hasSuccess ? "text-green-500 hover:bg-green-50 dark:hover:bg-green-950" :
"text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
{showPassword ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</motion.button>
)}
{/* Right Icon */}
{icon && iconPosition === "right" && !showPasswordToggle && (
<motion.div
className={cn(
"z-10",
hasError ? "text-red-500" :
hasSuccess ? "text-green-500" :
"text-gray-400"
)}
animate={{
scale: isFocused ? 1.1 : 1,
x: isFocused ? 2 : 0,
}}
transition={{ duration: 0.2 }}
>
{icon}
</motion.div>
)}
</div>
{/* Focus Ring Animation */}
<motion.div
className="absolute inset-0 rounded-md pointer-events-none"
animate={{
boxShadow: isFocused
? hasError
? "0 0 0 2px rgba(239, 68, 68, 0.2)"
: hasSuccess
? "0 0 0 2px rgba(34, 197, 94, 0.2)"
: "0 0 0 2px rgba(59, 130, 246, 0.2)"
: "0 0 0 0px rgba(0, 0, 0, 0)",
}}
transition={{ duration: 0.2 }}
/>
</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>
{/* Success Message */}
<AnimatePresence>
{hasSuccess && !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-green-600 dark:text-green-400 flex items-center gap-1"
>
<Check className="w-4 h-4 flex-shrink-0" />
Looks good!
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
export default Input;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| type | 'text' | 'password' | 'email' | 'search' | 'number' | 'tel' | 'url' | 'text' | The input type. |
| variant | 'default' | 'filled' | 'outlined' | 'underlined' | 'default' | The visual style variant of the input. |
| size | 'sm' | 'md' | 'lg' | 'md' | The size of the input. |
| label | string | undefined | Label text for the input. |
| placeholder | string | undefined | Placeholder text. |
| error | string | undefined | Error message to display. |
| success | boolean | false | Show success state. |
| disabled | boolean | false | Disable the input. |
| required | boolean | false | Mark input as required. |
| icon | React.ReactNode | undefined | Icon to display in the input. |
| iconPosition | 'left' | 'right' | 'left' | Position of the icon. |
| showPasswordToggle | boolean | false | Show password visibility toggle (for password type). |