Skip to main content

Overview

The Input component is a fully-featured text input field with support for labels, validation errors, helper text, icons, and different sizes. It includes focus state management and accessibility features.

TypeScript Types

InputSize

type InputSize = 'sm' | 'md' | 'lg';

InputProps

interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
  label?: string;
  error?: string;
  helperText?: string;
  size?: InputSize;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}
The size prop from the standard InputHTMLAttributes is omitted to avoid conflicts with our custom InputSize type.

Props

label
string
Label text displayed above the input field. The label is properly associated with the input using the htmlFor attribute for accessibility.
error
string
Error message to display below the input. When present:
  • Input border turns red (border-destructive)
  • Label text becomes red
  • Icons turn red
  • Error message appears below in red text
  • Takes precedence over helperText
helperText
string
Helper text displayed below the input in muted color. Useful for providing hints or additional context. Hidden when error is present.
size
InputSize
default:"md"
The size of the input field:
  • sm - Small (px-3 py-1.5 text-sm)
  • md - Medium (px-4 py-2.5 text-base)
  • lg - Large (px-5 py-3.5 text-lg)
leftIcon
React.ReactNode
Icon or element to display on the left side inside the input. The input padding is automatically adjusted to accommodate the icon.
rightIcon
React.ReactNode
Icon or element to display on the right side inside the input. The input padding is automatically adjusted to accommodate the icon.
disabled
boolean
default:"false"
When true, the input is disabled with reduced opacity and a not-allowed cursor. The background changes to muted.
className
string
Additional CSS classes to apply to the input element.
id
string
Custom ID for the input element. If not provided, a unique ID is automatically generated using React’s useId hook.

Basic Usage

import { Input } from '@/components/Input';

<Input 
  label="Email Address" 
  placeholder="Enter your email" 
  type="email"
/>

Input States

<Input 
  label="Username" 
  placeholder="Enter your username" 
/>
The default state features:
  • Card background (bg-card)
  • Border color transitions from border-input to border-input-focus on focus
  • Focus ring with primary color
  • Label transitions to primary color on focus

Icon Examples

Left Icon

import { Search } from 'lucide-react';

<Input 
  label="Search" 
  placeholder="Search..."
  leftIcon={<Search size={18} />}
/>
Icon styling:
  • Positioned absolutely on the left
  • Muted color by default
  • Transitions to primary color on focus
  • Transitions to destructive color on error
  • Input padding automatically adjusted

Right Icon

import { Eye } from 'lucide-react';

<Input 
  label="Password" 
  type="password"
  placeholder="Enter password"
  rightIcon={<Eye size={18} />}
/>

Both Icons

import { Mail, Check } from 'lucide-react';

<Input 
  label="Email" 
  type="email"
  leftIcon={<Mail size={18} />}
  rightIcon={<Check size={18} />}
/>

Size Variants

<Input size="sm" placeholder="Small input" />
<Input size="md" placeholder="Medium input" />
<Input size="lg" placeholder="Large input" />
Size affects padding, font size, and icon positioning:
  • Small: px-3 py-1.5 text-sm (icon padding: pl-9/pr-9)
  • Medium: px-4 py-2.5 text-base (icon padding: pl-11/pr-11)
  • Large: px-5 py-3.5 text-lg (icon padding: pl-12/pr-12)

Form Integration

import { Input } from '@/components/Input';
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({ email: '', password: '' });
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // Validation
    const newErrors = { email: '', password: '' };
    
    if (!email) {
      newErrors.email = 'Email is required';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      newErrors.email = 'Invalid email format';
    }
    
    if (!password) {
      newErrors.password = 'Password is required';
    } else if (password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }
    
    setErrors(newErrors);
    
    if (!newErrors.email && !newErrors.password) {
      // Submit form
      console.log('Form submitted', { email, password });
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <Input
        label="Email"
        type="email"
        placeholder="[email protected]"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        error={errors.email}
      />
      
      <Input
        label="Password"
        type="password"
        placeholder="Enter password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        error={errors.password}
        helperText="Minimum 8 characters"
      />
      
      <button type="submit">Login</button>
    </form>
  );
}

Complete Example

import { Input } from '@/components/Input';
import { Search, Mail } from 'lucide-react';

function InputExamples() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
      {/* Normal state */}
      <Input
        label="Normal State"
        placeholder="Type something..."
      />
      
      {/* With left icon */}
      <Input
        label="With Icon"
        placeholder="Search..."
        leftIcon={<Search size={18} />}
      />
      
      {/* With error */}
      <Input
        label="With Error"
        placeholder="invalid@email"
        error="This field is required"
        defaultValue="invalid-email"
      />
      
      {/* Disabled */}
      <Input
        label="Disabled"
        placeholder="Not available"
        disabled
      />
      
      {/* With helper text */}
      <Input
        label="With Helper Text"
        type="password"
        placeholder="Password"
        helperText="Minimum 8 characters"
      />
      
      {/* Large size */}
      <Input
        label="Large Size"
        placeholder="Large input"
        size="lg"
      />
    </div>
  );
}

Focus State Management

The Input component uses internal state to track focus:
  • Label color changes to primary on focus
  • Border color transitions to focus color
  • Focus ring appears
  • Icons change to primary color (or destructive if error)
  • Respects custom onFocus and onBlur handlers
<Input
  label="Username"
  onFocus={(e) => console.log('Input focused')}
  onBlur={(e) => console.log('Input blurred')}
/>

Accessibility

  • Uses semantic <input> element with proper type attribute
  • Label is associated with input via htmlFor and unique ID
  • Error messages are displayed visually and in the DOM
  • Disabled state prevents interaction and is announced by screen readers
  • Placeholder text provides hints but is not a replacement for labels
  • Focus states are clearly visible for keyboard navigation

Best Practices

Labels are essential for accessibility. Even if the design doesn’t show a visible label, consider using the label prop for screen readers or providing an aria-label.
Helper text should provide useful context or formatting hints (e.g., “Minimum 8 characters”, “Format: MM/DD/YYYY”).
Use the error prop to display specific, actionable error messages that help users fix validation issues.
Use the correct type attribute (email, password, tel, number, etc.) to enable proper mobile keyboards and browser validation.
Keep icon sizes consistent with the input size:
  • Small inputs: 16-18px icons
  • Medium inputs: 18-20px icons
  • Large inputs: 20-22px icons

Build docs developers (and LLMs) love