Input
Labeled input with error state, hint text, left/right icon slots, sanitized onChange, and full accessibility.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | undefined | Rendered as <label> above the input |
| error | string | undefined | Red helper text below input, animate-fade-in |
| hint | string | undefined | Muted helper text (hidden when error is shown) |
| leftIcon | ReactNode | undefined | Absolutely positioned inside input, left side |
| rightIcon | ReactNode | undefined | Absolutely positioned inside input, right side |
| ...props | InputHTMLAttributes | — | All standard input props |
Error state
When the error prop is set, the input border changes to --color-error and the error message appears with animate-fade-in:
<Input label="Email" type="email" error="Please enter a valid email address" />
fieldErrors from ActionState map directly to error props:
<Input
label="Email"
name="email"
type="email"
error={fieldErrors?.email?.[0]}
/>Icon slots
import { Mail, Eye, EyeOff } from 'lucide-react'
<Input
label="Email"
type="email"
leftIcon={<Mail size={15} />}
/>
<Input
label="Password"
type={showPassword ? 'text' : 'password'}
rightIcon={
<button
type="button"
onClick={() => setShowPassword(v => !v)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={15} /> : <Eye size={15} />}
</button>
}
/>Usage
components/auth/LoginForm.tsx
<Input
label="Email address"
name="email"
type="email"
placeholder="you@example.com"
autoComplete="email"
error={fieldErrors?.email?.[0]}
leftIcon={<Mail size={15} />}
disabled={isPending}
required
/>Client-side sanitization
Input's onChange handler trims the value and enforces maxLength before calling the original onChange. This is a lightweight client-side guard — it does not replace server-side sanitization in server actions.
Server-side sanitization via sanitizeAndStrip() is the primary defense. The client-side trim in Input is a UX convenience, not a security boundary.