Dependencies
tailwindcssshadcn/uilucide-react
shadcn/ui components
npx shadcn@latest add inputnpx shadcn@latest add labelnpx shadcn@latest add button

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.

tsx
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.

tsx
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.

tsx
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.

tsx
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&apos;t receive the code?{" "}
          <button className="font-semibold" style={{ color: "#fd4766" }}>Resend</button>
        </p>
      </div>
    </div>
  )
}

Frequently Asked Questions

Wrap the Input in a relative div. Position the icon absolute inside: className='absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground'. Add pl-9 to the Input to make space: <Input className='pl-9'>.
Add border-destructive and focus-visible:ring-destructive to the className: <Input className='border-destructive focus-visible:ring-destructive'>. Add an error message below: <p className='text-xs text-destructive'>Error message</p>.
Input is already full width by default — it has w-full in its base styles. If it's not filling its container, check the parent element has a defined width or is a block element.
Use a flex container. Give the addon a border and bg-muted background, remove the adjacent border with border-r-0 or border-l-0. Round only the outer corners: rounded-l-md on the prefix, no rounding on the input, rounded-r-md on the suffix.

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.

Related Components