Input

Labeled input with error state, hint text, left/right icon slots, sanitized onChange, and full accessibility.

Props

PropTypeDefaultDescription
labelstringundefinedRendered as <label> above the input
errorstringundefinedRed helper text below input, animate-fade-in
hintstringundefinedMuted helper text (hidden when error is shown)
leftIconReactNodeundefinedAbsolutely positioned inside input, left side
rightIconReactNodeundefinedAbsolutely positioned inside input, right side
...propsInputHTMLAttributesAll 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.