shadcn/ui Input is a clean, accessible text input component. These 4 examples cover the most common input patterns including icons, addons, states and OTP entry.
Installation
npx shadcn@latest add input label button
npm install lucide-react
Basic Usage
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
<div className="space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>
1. React Input with Icons
Input fields with leading and trailing icons using absolute positioning inside a relative wrapper.
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Search, Mail, Lock, Eye, EyeOff, User, Phone } from "lucide-react"
import { useState } from "react"
function InputWithIcon({
label,
placeholder,
type = "text",
leadingIcon: LeadingIcon,
trailingIcon: TrailingIcon,
onTrailingClick,
}: {
label: string
placeholder: string
type?: string
leadingIcon?: React.ElementType
trailingIcon?: React.ElementType
onTrailingClick?: () => void
}) {
return (
<div className="space-y-1.5">
<Label>{label}</Label>
<div className="relative">
{LeadingIcon && (
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
<LeadingIcon size={16} />
</div>
)}
<Input
type={type}
placeholder={placeholder}
className={`${LeadingIcon ? "pl-9" : ""} ${TrailingIcon ? "pr-9" : ""}`}
/>
{TrailingIcon && (
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={onTrailingClick}
>
<TrailingIcon size={16} />
</button>
)}
</div>
</div>
)
}
export default function InputWithIcons() {
const [showPwd, setShowPwd] = useState(false)
return (
<div className="space-y-4 max-w-sm">
<InputWithIcon label="Search" placeholder="Search templates..." leadingIcon={Search} />
<InputWithIcon label="Email" placeholder="you@example.com" type="email" leadingIcon={Mail} />
<InputWithIcon label="Username" placeholder="gagansingh" leadingIcon={User} />
<InputWithIcon label="Phone" placeholder="+91 98765 43210" leadingIcon={Phone} />
<InputWithIcon
label="Password"
placeholder="••••••••"
type={showPwd ? "text" : "password"}
leadingIcon={Lock}
trailingIcon={showPwd ? EyeOff : Eye}
onTrailingClick={() => setShowPwd(!showPwd)}
/>
</div>
)
}2. React Input with Addons
Input groups with text prefix/suffix addons and button addons.
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Copy, ExternalLink } from "lucide-react"
import { useState } from "react"
function AddonInput({
label,
prefix,
suffix,
placeholder,
buttonLabel,
onButton,
}: {
label: string
prefix?: string
suffix?: string
placeholder?: string
buttonLabel?: string
onButton?: () => void
}) {
return (
<div className="space-y-1.5">
<Label>{label}</Label>
<div className="flex">
{prefix && (
<div className="flex items-center px-3 border border-r-0 rounded-l-md bg-muted text-muted-foreground text-sm whitespace-nowrap">
{prefix}
</div>
)}
<Input
placeholder={placeholder}
className={`${prefix ? "rounded-l-none" : ""} ${suffix || buttonLabel ? "rounded-r-none" : ""}`}
/>
{suffix && (
<div className="flex items-center px-3 border border-l-0 rounded-r-md bg-muted text-muted-foreground text-sm whitespace-nowrap">
{suffix}
</div>
)}
{buttonLabel && (
<Button className="rounded-l-none" onClick={onButton} style={{ background: "#fd4766" }}>
{buttonLabel}
</Button>
)}
</div>
</div>
)
}
export default function InputAddons() {
const [copied, setCopied] = useState(false)
const copyToClipboard = () => {
navigator.clipboard.writeText("https://lettstartdesign.com")
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="space-y-4 max-w-sm">
<AddonInput label="Website URL" prefix="https://" placeholder="yoursite.com" />
<AddonInput label="Username" prefix="@" placeholder="gagansingh" />
<AddonInput label="Price" prefix="$" suffix="USD" placeholder="29.00" />
<AddonInput label="Domain" prefix="https://" suffix=".com" placeholder="yoursite" />
{/* Referral link with copy button */}
<div className="space-y-1.5">
<Label>Referral Link</Label>
<div className="flex">
<Input
defaultValue="https://lettstartdesign.com?ref=FIRST30"
readOnly
className="rounded-r-none text-muted-foreground text-xs"
/>
<Button
variant="outline"
className="rounded-l-none border-l-0 flex-shrink-0"
onClick={copyToClipboard}
>
<Copy size={14} />
<span className="ml-1.5 text-xs">{copied ? "Copied!" : "Copy"}</span>
</Button>
</div>
</div>
<AddonInput label="Subscribe" placeholder="your@email.com" buttonLabel="Subscribe" />
</div>
)
}3. React Input States and Sizes
Input fields in all states — default, focus, error, success, disabled — and all three sizes.
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { CheckCircle2, AlertCircle } from "lucide-react"
export default function InputStates() {
return (
<div className="space-y-6 max-w-sm">
{/* Sizes */}
<div className="space-y-3">
<p className="text-sm font-semibold">Sizes</p>
<Input className="h-8 text-sm" placeholder="Small input (h-8)" />
<Input placeholder="Default input (h-10)" />
<Input className="h-12 text-base" placeholder="Large input (h-12)" />
</div>
{/* States */}
<div className="space-y-3">
<p className="text-sm font-semibold">States</p>
<div className="space-y-1">
<Label>Default</Label>
<Input placeholder="Default state" />
</div>
<div className="space-y-1">
<Label>Disabled</Label>
<Input placeholder="Disabled — cannot edit" disabled />
</div>
<div className="space-y-1">
<Label>Read Only</Label>
<Input value="Read only value" readOnly className="bg-muted" />
</div>
<div className="space-y-1">
<Label className="text-destructive">Error State</Label>
<div className="relative">
<Input
placeholder="Invalid email"
defaultValue="not-an-email"
className="border-destructive pr-9 focus-visible:ring-destructive"
/>
<AlertCircle size={16} className="absolute right-3 top-1/2 -translate-y-1/2 text-destructive" />
</div>
<p className="text-xs text-destructive">Please enter a valid email address</p>
</div>
<div className="space-y-1">
<Label className="text-green-600">Success State</Label>
<div className="relative">
<Input
defaultValue="gagan@example.com"
className="border-green-500 pr-9 focus-visible:ring-green-500"
/>
<CheckCircle2 size={16} className="absolute right-3 top-1/2 -translate-y-1/2 text-green-500" />
</div>
<p className="text-xs text-green-600">Email is available</p>
</div>
</div>
</div>
)
}4. React OTP / PIN Input
6-digit OTP input where focus moves automatically to the next field on input.
import { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
export default function OTPInput() {
const [otp, setOtp] = useState(Array(6).fill(""))
const refs = Array(6).fill(null).map(() => useRef<HTMLInputElement>(null))
const [verified, setVerified] = useState(false)
const handleChange = (index: number, value: string) => {
if (!/^\d*$/.test(value)) return
const newOtp = [...otp]
newOtp[index] = value.slice(-1)
setOtp(newOtp)
if (value && index < 5) refs[index + 1].current?.focus()
}
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
if (e.key === "Backspace" && !otp[index] && index > 0) {
refs[index - 1].current?.focus()
}
}
const handlePaste = (e: React.ClipboardEvent) => {
const paste = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, 6)
const newOtp = [...otp]
paste.split("").forEach((char, i) => { newOtp[i] = char })
setOtp(newOtp)
refs[Math.min(paste.length, 5)].current?.focus()
}
const code = otp.join("")
const isComplete = code.length === 6
return (
<div className="space-y-6 max-w-xs">
<div>
<p className="text-sm font-semibold mb-1">Verify Your Email</p>
<p className="text-xs text-muted-foreground mb-4">
Enter the 6-digit code sent to gagan@example.com
</p>
<div className="flex gap-2 mb-4">
{otp.map((digit, index) => (
<input
key={index}
ref={refs[index]}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={index === 0 ? handlePaste : undefined}
className="w-11 h-12 text-center text-lg font-bold border-2 rounded-lg outline-none transition-colors"
style={{
borderColor: digit ? "#fd4766" : "#e5e7eb",
color: digit ? "#fd4766" : undefined,
}}
/>
))}
</div>
{verified ? (
<div className="flex items-center gap-2 text-green-600 text-sm font-semibold">
✅ Email verified successfully!
</div>
) : (
<Button
className="w-full"
style={{ background: "#fd4766" }}
disabled={!isComplete}
onClick={() => setVerified(true)}
>
{isComplete ? "Verify Code" : `Enter ${6 - code.length} more digits`}
</Button>
)}
<p className="text-xs text-center text-muted-foreground mt-3">
Didn't receive the code?{" "}
<button className="font-semibold" style={{ color: "#fd4766" }}>Resend</button>
</p>
</div>
</div>
)
}Frequently Asked Questions
Need a Full React + Next.js Dashboard Template?
Get a complete React + Next.js dashboard with 50+ components — built by the same team behind BootstrapPlanet.
Browse Templates →Use code FIRST30 for 30% off.