Input
A labeled text field with built-in states for validation, icons, and helper text. Extends the native input element so all HTML attributes pass through.
Preview
Sizes
Three sizes to fit any density — compact toolbars, standard forms, or prominent search bars.
Icons
Left icons set context; right icons signal supplementary actions or status. Both are pointer-events-none by default.
States
Error replaces helperText and switches the ring to red. Disabled removes all interaction and dims to 50%.
3–24 characters, no spaces.
That email is already taken.
Implementation
Built with forwardRef so refs work transparently. All standard HTML input attributes are forwarded through.
import { Input } from "@/components/ui/Input";
import { Search, Mail, Lock } from "lucide-react";
// Basic
<Input label="Email" placeholder="you@example.com" type="email" />
// With helper text
<Input
label="Username"
helperText="Must be 3–24 characters."
placeholder="sitka_user"
/>
// Error state
<Input
label="Password"
error="Must be at least 8 characters."
type="password"
defaultValue="abc"
/>
// With icons
<Input
label="Search"
leftIcon={<Search className="w-4 h-4" />}
placeholder="Search components…"
/>
<Input
label="Email"
leftIcon={<Mail className="w-4 h-4" />}
rightIcon={<Lock className="w-3.5 h-3.5" />}
placeholder="you@example.com"
type="email"
/>
// Sizes
<Input inputSize="sm" placeholder="Small" />
<Input inputSize="md" placeholder="Medium" />
<Input inputSize="lg" placeholder="Large" />
// Disabled
<Input label="Read-only field" disabled defaultValue="Cannot edit this" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Renders a <label> element associated with the input via id. |
helperText | string | — | Subtle hint text shown below the input when there is no error. |
error | string | — | Error message. Switches border and ring to red and replaces helperText. |
inputSize | "sm" | "md" | "lg" | "md" | Controls height, padding, and font size. |
leftIcon | ReactNode | — | Icon node rendered inside the left edge of the input. |
rightIcon | ReactNode | — | Icon node rendered inside the right edge of the input. |
disabled | boolean | false | Dims the input and removes interaction. |
Mobile
Inputs on mobile trigger the virtual keyboard and interact directly with the OS input system. Getting these details right prevents zoom jank and missed taps.
Sign in
Welcome back
| Scenario | Guidance |
|---|---|
| Prevent zoom on focus | Keep font-size ≥ 16px on the <input> element. iOS zooms in when the font is smaller. The lg size meets this; md (13px) does not — add font-size: 16px in mobile CSS. |
| inputmode attribute | Pass inputMode="email" for emails, "numeric" or "decimal" for numbers, "tel" for phone numbers, and "url" for URLs. This surfaces the right virtual keyboard without changing validation type. |
| Full-width on mobile | Inputs should be full-width (w-full) on small screens. Avoid fixed widths like w-72 in mobile layouts. |
| returnKeyType | Use type="search" for search fields — mobile browsers show a Search/Go key. For form fields followed by another input, iOS advances focus on Next automatically. |
| Autofill | Add autocomplete attributes ("email", "current-password", "given-name", etc.) so iOS/Android Autofill and password managers work correctly. |
| Touch target height | Use inputSize="md" (h-10) or larger. The sm size (h-8 / 32px) is below the 44px guideline — only use it in dense UI where labels confirm context. |
Accessibility
- →The label prop auto-generates an id and wires the <label> htmlFor — never skip it for form inputs.
- →Error strings are rendered in a <p> below the input; screen readers announce them on focus.
- →Disabled inputs are excluded from tab order natively — no aria-disabled workaround needed.
- →Icons are aria-hidden via pointer-events-none spans; they carry no semantic meaning.
- →Focus ring uses focus-visible so it only appears on keyboard navigation.