Skip to main content

Form Patterns

A collection of production-ready form patterns for building accessible, user-friendly forms. Each pattern demonstrates best practices for layout, validation, state management, and user experience.

Basic Form

A simple contact form with name, email, and message fields. Demonstrates controlled inputs with form submission.

Basic Contact Form
tsx
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');

<form onSubmit={handleSubmit} className="space-y-4">
  <div>
    <label className="block text-xs font-medium mb-1.5">Name</label>
    <input
      type="text"
      placeholder="John Doe"
      value={name}
      onChange={(e) => setName(e.target.value)}
      className="w-full bg-(--surface-sunken) border border-(--border)
        rounded-lg px-3 py-2 text-sm"
      required
    />
  </div>
  <div>
    <label>Email</label>
    <input type="email" value={email} onChange={...} />
  </div>
  <div>
    <label>Message</label>
    <textarea value={message} onChange={...} rows={4} />
  </div>
  <button type="submit">Send Message</button>
</form>

Inline Validation

Form fields with real-time validation feedback. Fields show green for valid input and red for errors as the user types.

Inline Validation
tsx
const [email, setEmail] = useState('');
const emailValid = email.length === 0
  ? null
  : /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

<div>
  <label>Email</label>
  <input
    type="email"
    value={email}
    onChange={(e) => setEmail(e.target.value)}
    className={emailValid === null
      ? inputClass
      : emailValid ? inputSuccessClass : inputErrorClass}
  />
  {emailValid === false && (
    <p className="text-xs text-danger-500 mt-1">
      Please enter a valid email
    </p>
  )}
  {emailValid === true && (
    <p className="text-xs text-green-500 mt-1">Valid email</p>
  )}
</div>

Multi-Step Form

A stepper form that breaks complex flows into manageable steps. Includes a progress indicator, navigation controls, and step validation.

1
Personal
2
Company
3
Plan
Multi-Step Form
tsx
const [currentStep, setCurrentStep] = useState(0);
const steps = ['Personal', 'Company', 'Plan'];

{/* Steps indicator */}
<div className="flex items-center justify-between">
  {steps.map((step, i) => (
    <div key={step} className="flex items-center">
      <div className={`w-8 h-8 rounded-full flex items-center
        justify-center ${i <= currentStep
          ? 'bg-brand-500 text-white'
          : 'bg-(--surface-sunken) border border-(--border)'
        }`}>
        {i < currentStep ? <CheckIcon /> : i + 1}
      </div>
      {i < steps.length - 1 && (
        <div className={`w-24 h-0.5 ${i < currentStep
          ? 'bg-brand-500' : 'bg-(--border)'}`} />
      )}
    </div>
  ))}
</div>

{/* Step content with AnimatePresence */}
{currentStep === 0 && <StepPersonal />}
{currentStep === 1 && <StepCompany />}
{currentStep === 2 && <StepPlan />}

{/* Navigation */}
<button onClick={() => setCurrentStep(s => s - 1)}>Previous</button>
<button onClick={() => setCurrentStep(s => s + 1)}>Next</button>

Search & Filter

A search input combined with filter chips for content discovery. Filter chips allow toggling multiple categories.

Search & Filter
tsx
const [searchQuery, setSearchQuery] = useState('');
const [activeFilters, setActiveFilters] = useState(['All']);
const filters = ['All', 'Components', 'Patterns', 'Foundation'];

<div className="relative">
  <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2" />
  <input
    placeholder="Search..."
    value={searchQuery}
    onChange={(e) => setSearchQuery(e.target.value)}
    className="pl-10 ..."
  />
</div>

<div className="flex flex-wrap gap-2">
  {filters.map((filter) => (
    <button
      key={filter}
      onClick={() => toggleFilter(filter)}
      className={activeFilters.includes(filter)
        ? 'bg-brand-500/10 text-brand-500'
        : 'text-(--text-muted) border-(--border)'}
    >
      {filter}
    </button>
  ))}
</div>

Settings Form

A settings page with toggle switches, select dropdowns, and grouped sections for organizing preferences.

Appearance

Dark Mode

Toggle dark theme

Notifications

Email Notifications

Receive updates via email

Settings Form
tsx
const [darkMode, setDarkMode] = useState(false);
const [language, setLanguage] = useState('en');

<div className="space-y-6">
  <div>
    <h3 className="text-sm font-semibold">Appearance</h3>
    <div className="p-4 rounded-lg border bg-(--surface-sunken)">
      {/* Toggle Switch */}
      <div className="flex items-center justify-between">
        <span>Dark Mode</span>
        <button
          onClick={() => setDarkMode(!darkMode)}
          className={`relative w-10 h-5.5 rounded-full
            ${darkMode ? 'bg-brand-500' : 'bg-(--border)'}`}
        >
          <span className={`w-4 h-4 rounded-full bg-white
            ${darkMode ? 'translate-x-5' : 'translate-x-0.75'}`} />
        </button>
      </div>

      {/* Select Dropdown */}
      <select value={language} onChange={...}>
        <option value="en">English</option>
        <option value="id">Bahasa Indonesia</option>
      </select>
    </div>
  </div>
</div>

Login Form

A classic login form with email and password fields, remember-me checkbox, and a forgot password link.

Welcome back

Sign in to your account to continue

Don't have an account? Sign up

Login Form
tsx
<div className="p-6 rounded-xl border bg-(--surface-sunken)">
  <h3>Welcome back</h3>
  <p>Sign in to your account to continue</p>

  <form onSubmit={handleSubmit}>
    <div>
      <label>Email</label>
      <input type="email" placeholder="you@example.com" />
    </div>
    <div>
      <div className="flex justify-between">
        <label>Password</label>
        <a href="#">Forgot password?</a>
      </div>
      <input type="password" />
    </div>
    <div className="flex items-center gap-2">
      <input type="checkbox" id="remember" />
      <label htmlFor="remember">Remember me</label>
    </div>
    <button type="submit">Sign In</button>
  </form>

  <p>Don't have an account? <a href="#">Sign up</a></p>
</div>

Form Layout

Comparison of horizontal (inline) and vertical (stacked) label placement. Horizontal works best on wider screens, vertical is more mobile-friendly.

Vertical (Stacked) Labels

Horizontal (Inline) Labels

Form Layout
tsx
{/* Vertical (Stacked) — default, mobile-friendly */}
<div className="space-y-4">
  <div>
    <label className="block text-xs font-medium mb-1.5">
      First Name
    </label>
    <input className="w-full ..." />
  </div>
</div>

{/* Horizontal (Inline) — wider screens */}
<div className="space-y-4">
  <div className="flex items-center gap-4">
    <label className="w-24 text-right flex-shrink-0">
      First Name
    </label>
    <input className="w-full ..." />
  </div>
</div>

Disabled States

Forms in disabled or read-only mode. Use disabled for fields that cannot be changed, and read-only for fields that are informational.

This field is read-only and cannot be edited

Tip
Use disabled when the field cannot be interacted with at all (grayed out, not focusable). Use readOnly when the value should be visible and selectable but not editable.
Disabled & Read-Only
tsx
{/* Disabled — grayed out, not focusable */}
<input
  type="text"
  value="Jane Smith"
  disabled
  className="... opacity-60 cursor-not-allowed"
/>

{/* Read-only — selectable but not editable */}
<input
  type="text"
  value="Administrator"
  readOnly
  className="... cursor-default"
/>

Best Practices

Guidelines for building effective, accessible, and user-friendly forms.

Do
  • Always provide visible labels for every form field
  • Show validation errors inline near the relevant field
  • Use appropriate input types (email, tel, url, number)
  • Group related fields together with clear section headings
  • Break long forms into multi-step flows with progress indicators
  • Provide clear, actionable error messages
Don't
  • Use placeholder text as a replacement for labels
  • Validate every keystroke without debouncing
  • Show all errors at the top of the form only
  • Use generic error messages like "Invalid input"
  • Ask for unnecessary information or too many fields
  • Rely solely on color to indicate validation state

Components commonly used together when building forms.