Build beautiful apps
Start

Forms

FlexiUI provides powerful form components built on React Aria, ensuring accessibility and proper form handling out of the box. This guide covers everything from basic forms to advanced patterns with form validation libraries.

Core Concepts

FlexiUI form components are designed with these principles:

  • Accessibility First - Full ARIA support and keyboard navigation
  • Native Validation - Built-in HTML5 validation support
  • Framework Agnostic - Works with any form library or vanilla React
  • Type Safe - Full TypeScript support for form data
  • Controlled & Uncontrolled - Support for both patterns

Basic Forms

Simple Form

Create a basic form using FlexiUI components:

import { Form } from '@flexi-ui/form'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function ContactForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    const data = Object.fromEntries(formData)
    console.log('Form data:', data)
  }
 
  return (
    <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <Input
        label="Name"
        name="name"
        placeholder="Enter your name"
        isRequired
      />
      <Input
        label="Email"
        name="email"
        type="email"
        placeholder="Enter your email"
        isRequired
      />
      <Input
        label="Message"
        name="message"
        placeholder="Enter your message"
        isRequired
      />
      <Button type="submit" color="primary">
        Submit
      </Button>
    </Form>
  )
}

Form with Validation Messages

Add error messages for better user feedback:

import { Form } from '@flexi-ui/form'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function ValidationForm() {
  return (
    <Form className="flex flex-col gap-4">
      <Input
        label="Username"
        name="username"
        isRequired
        errorMessage="Username is required"
        description="Choose a unique username"
      />
      <Input
        label="Email"
        name="email"
        type="email"
        isRequired
        errorMessage="Please enter a valid email address"
      />
      <Input
        label="Password"
        name="password"
        type="password"
        isRequired
        minLength={8}
        errorMessage="Password must be at least 8 characters"
      />
      <Button type="submit" color="primary">
        Create Account
      </Button>
    </Form>
  )
}

Form State Management

Controlled Components

Use React state to control form inputs:

'use client'
 
import { useState } from 'react'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function ControlledForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: ''
  })
 
  const handleChange = (field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }))
  }
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    console.log('Submitted:', formData)
  }
 
  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <Input
        label="Name"
        value={formData.name}
        onChange={handleChange('name')}
      />
      <Input
        label="Email"
        type="email"
        value={formData.email}
        onChange={handleChange('email')}
      />
      <Input
        label="Phone"
        type="tel"
        value={formData.phone}
        onChange={handleChange('phone')}
      />
      <Button type="submit" color="primary">
        Submit
      </Button>
    </form>
  )
}

Uncontrolled Components

Use refs for uncontrolled form inputs:

'use client'
 
import { useRef } from 'react'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function UncontrolledForm() {
  const formRef = useRef<HTMLFormElement>(null)
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (formRef.current) {
      const formData = new FormData(formRef.current)
      const data = Object.fromEntries(formData)
      console.log('Submitted:', data)
    }
  }
 
  return (
    <form ref={formRef} onSubmit={handleSubmit} className="flex flex-col gap-4">
      <Input label="Name" name="name" defaultValue="" />
      <Input label="Email" name="email" type="email" defaultValue="" />
      <Button type="submit" color="primary">
        Submit
      </Button>
    </form>
  )
}

Form Validation

HTML5 Validation

Leverage native HTML5 validation:

import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function HTML5Validation() {
  return (
    <form className="flex flex-col gap-4">
      {/* Required field */}
      <Input
        label="Name"
        name="name"
        isRequired
        errorMessage="Name is required"
      />
 
      {/* Email validation */}
      <Input
        label="Email"
        name="email"
        type="email"
        isRequired
        errorMessage="Please enter a valid email"
      />
 
      {/* Minimum length */}
      <Input
        label="Password"
        name="password"
        type="password"
        minLength={8}
        isRequired
        errorMessage="Password must be at least 8 characters"
      />
 
      {/* Pattern validation */}
      <Input
        label="Phone"
        name="phone"
        type="tel"
        pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
        placeholder="123-456-7890"
        errorMessage="Please enter a valid phone number (XXX-XXX-XXXX)"
      />
 
      {/* Number range */}
      <Input
        label="Age"
        name="age"
        type="number"
        min={18}
        max={100}
        errorMessage="Age must be between 18 and 100"
      />
 
      <Button type="submit" color="primary">
        Submit
      </Button>
    </form>
  )
}

Custom Validation

Implement custom validation logic:

'use client'
 
import { useState } from 'react'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function CustomValidation() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')
 
  const [errors, setErrors] = useState<Record<string, string>>({})
 
  const validate = () => {
    const newErrors: Record<string, string> = {}
 
    // Email validation
    if (!email) {
      newErrors.email = 'Email is required'
    } else if (!/\S+@\S+\.\S+/.test(email)) {
      newErrors.email = 'Email is invalid'
    }
 
    // Password validation
    if (!password) {
      newErrors.password = 'Password is required'
    } else if (password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters'
    } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
      newErrors.password = 'Password must contain uppercase, lowercase, and number'
    }
 
    // Confirm password validation
    if (password !== confirmPassword) {
      newErrors.confirmPassword = 'Passwords do not match'
    }
 
    setErrors(newErrors)
    return Object.keys(newErrors).length === 0
  }
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (validate()) {
      console.log('Form is valid!')
    }
  }
 
  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <Input
        label="Email"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        isInvalid={!!errors.email}
        errorMessage={errors.email}
      />
      <Input
        label="Password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        isInvalid={!!errors.password}
        errorMessage={errors.password}
        description="Must be 8+ characters with uppercase, lowercase, and number"
      />
      <Input
        label="Confirm Password"
        type="password"
        value={confirmPassword}
        onChange={(e) => setConfirmPassword(e.target.value)}
        isInvalid={!!errors.confirmPassword}
        errorMessage={errors.confirmPassword}
      />
      <Button type="submit" color="primary">
        Sign Up
      </Button>
    </form>
  )
}

Form Libraries Integration

React Hook Form

FlexiUI works seamlessly with React Hook Form:

npm install react-hook-form
'use client'
 
import { useForm } from 'react-hook-form'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
interface FormData {
  name: string
  email: string
  password: string
}
 
export default function ReactHookFormExample() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<FormData>()
 
  const onSubmit = async (data: FormData) => {
    await new Promise(resolve => setTimeout(resolve, 2000))
    console.log('Submitted:', data)
  }
 
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
      <Input
        label="Name"
        {...register('name', {
          required: 'Name is required',
          minLength: {
            value: 2,
            message: 'Name must be at least 2 characters'
          }
        })}
        isInvalid={!!errors.name}
        errorMessage={errors.name?.message}
      />
      <Input
        label="Email"
        type="email"
        {...register('email', {
          required: 'Email is required',
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: 'Email is invalid'
          }
        })}
        isInvalid={!!errors.email}
        errorMessage={errors.email?.message}
      />
      <Input
        label="Password"
        type="password"
        {...register('password', {
          required: 'Password is required',
          minLength: {
            value: 8,
            message: 'Password must be at least 8 characters'
          }
        })}
        isInvalid={!!errors.password}
        errorMessage={errors.password?.message}
      />
      <Button
        type="submit"
        color="primary"
        isLoading={isSubmitting}
      >
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </Button>
    </form>
  )
}

React Hook Form with Zod

Combine React Hook Form with Zod for type-safe validation:

npm install react-hook-form zod @hookform/resolvers
'use client'
 
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
const schema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  age: z.number().min(18, 'Must be at least 18 years old'),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
    .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
    .regex(/[0-9]/, 'Password must contain at least one number'),
})
 
type FormData = z.infer<typeof schema>
 
export default function ZodValidation() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<FormData>({
    resolver: zodResolver(schema)
  })
 
  const onSubmit = async (data: FormData) => {
    console.log('Validated data:', data)
  }
 
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
      <Input
        label="Name"
        {...register('name')}
        isInvalid={!!errors.name}
        errorMessage={errors.name?.message}
      />
      <Input
        label="Email"
        type="email"
        {...register('email')}
        isInvalid={!!errors.email}
        errorMessage={errors.email?.message}
      />
      <Input
        label="Age"
        type="number"
        {...register('age', { valueAsNumber: true })}
        isInvalid={!!errors.age}
        errorMessage={errors.age?.message}
      />
      <Input
        label="Password"
        type="password"
        {...register('password')}
        isInvalid={!!errors.password}
        errorMessage={errors.password?.message}
      />
      <Button type="submit" color="primary" isLoading={isSubmitting}>
        Submit
      </Button>
    </form>
  )
}

Formik Integration

Use FlexiUI with Formik:

npm install formik
'use client'
 
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
const validationSchema = Yup.object({
  name: Yup.string()
    .min(2, 'Name must be at least 2 characters')
    .required('Name is required'),
  email: Yup.string()
    .email('Invalid email address')
    .required('Email is required'),
  password: Yup.string()
    .min(8, 'Password must be at least 8 characters')
    .required('Password is required'),
})
 
export default function FormikExample() {
  const formik = useFormik({
    initialValues: {
      name: '',
      email: '',
      password: '',
    },
    validationSchema,
    onSubmit: async (values) => {
      await new Promise(resolve => setTimeout(resolve, 2000))
      console.log('Submitted:', values)
    },
  })
 
  return (
    <form onSubmit={formik.handleSubmit} className="flex flex-col gap-4">
      <Input
        label="Name"
        name="name"
        value={formik.values.name}
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        isInvalid={formik.touched.name && !!formik.errors.name}
        errorMessage={formik.touched.name ? formik.errors.name : undefined}
      />
      <Input
        label="Email"
        name="email"
        type="email"
        value={formik.values.email}
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        isInvalid={formik.touched.email && !!formik.errors.email}
        errorMessage={formik.touched.email ? formik.errors.email : undefined}
      />
      <Input
        label="Password"
        name="password"
        type="password"
        value={formik.values.password}
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        isInvalid={formik.touched.password && !!formik.errors.password}
        errorMessage={formik.touched.password ? formik.errors.password : undefined}
      />
      <Button
        type="submit"
        color="primary"
        isLoading={formik.isSubmitting}
      >
        Submit
      </Button>
    </form>
  )
}

Advanced Patterns

Multi-Step Form

Create a multi-step form with validation at each step:

'use client'
 
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
interface Step1Data {
  firstName: string
  lastName: string
}
 
interface Step2Data {
  email: string
  phone: string
}
 
interface Step3Data {
  address: string
  city: string
  zipCode: string
}
 
export default function MultiStepForm() {
  const [step, setStep] = useState(1)
  const [formData, setFormData] = useState({})
 
  const { register, handleSubmit, formState: { errors } } = useForm()
 
  const onSubmit = (data: any) => {
    if (step < 3) {
      setFormData({ ...formData, ...data })
      setStep(step + 1)
    } else {
      const finalData = { ...formData, ...data }
      console.log('Final submission:', finalData)
    }
  }
 
  return (
    <div className="max-w-md mx-auto">
      {/* Progress indicator */}
      <div className="flex justify-between mb-8">
        {[1, 2, 3].map((s) => (
          <div
            key={s}
            className={`flex-1 h-2 mx-1 rounded ${
              s <= step ? 'bg-primary' : 'bg-default-200'
            }`}
          />
        ))}
      </div>
 
      <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
        {step === 1 && (
          <>
            <h2 className="text-xl font-semibold mb-2">Personal Information</h2>
            <Input
              label="First Name"
              {...register('firstName', { required: 'First name is required' })}
              isInvalid={!!errors.firstName}
              errorMessage={errors.firstName?.message as string}
            />
            <Input
              label="Last Name"
              {...register('lastName', { required: 'Last name is required' })}
              isInvalid={!!errors.lastName}
              errorMessage={errors.lastName?.message as string}
            />
          </>
        )}
 
        {step === 2 && (
          <>
            <h2 className="text-xl font-semibold mb-2">Contact Information</h2>
            <Input
              label="Email"
              type="email"
              {...register('email', {
                required: 'Email is required',
                pattern: {
                  value: /\S+@\S+\.\S+/,
                  message: 'Invalid email'
                }
              })}
              isInvalid={!!errors.email}
              errorMessage={errors.email?.message as string}
            />
            <Input
              label="Phone"
              type="tel"
              {...register('phone', { required: 'Phone is required' })}
              isInvalid={!!errors.phone}
              errorMessage={errors.phone?.message as string}
            />
          </>
        )}
 
        {step === 3 && (
          <>
            <h2 className="text-xl font-semibold mb-2">Address</h2>
            <Input
              label="Street Address"
              {...register('address', { required: 'Address is required' })}
              isInvalid={!!errors.address}
              errorMessage={errors.address?.message as string}
            />
            <Input
              label="City"
              {...register('city', { required: 'City is required' })}
              isInvalid={!!errors.city}
              errorMessage={errors.city?.message as string}
            />
            <Input
              label="ZIP Code"
              {...register('zipCode', { required: 'ZIP code is required' })}
              isInvalid={!!errors.zipCode}
              errorMessage={errors.zipCode?.message as string}
            />
          </>
        )}
 
        <div className="flex gap-2">
          {step > 1 && (
            <Button
              type="button"
              variant="bordered"
              onClick={() => setStep(step - 1)}
            >
              Previous
            </Button>
          )}
          <Button type="submit" color="primary" className="flex-1">
            {step === 3 ? 'Submit' : 'Next'}
          </Button>
        </div>
      </form>
    </div>
  )
}

Dynamic Field Arrays

Handle dynamic lists of fields:

'use client'
 
import { useFieldArray, useForm } from 'react-hook-form'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
interface FormData {
  users: {
    name: string
    email: string
  }[]
}
 
export default function FieldArrayExample() {
  const { register, control, handleSubmit } = useForm<FormData>({
    defaultValues: {
      users: [{ name: '', email: '' }]
    }
  })
 
  const { fields, append, remove } = useFieldArray({
    control,
    name: 'users'
  })
 
  const onSubmit = (data: FormData) => {
    console.log('Submitted:', data)
  }
 
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
      <h2 className="text-xl font-semibold">Add Users</h2>
 
      {fields.map((field, index) => (
        <div key={field.id} className="flex gap-2 items-start border-b pb-4">
          <div className="flex-1 flex flex-col gap-2">
            <Input
              label={`Name ${index + 1}`}
              {...register(`users.${index}.name` as const, {
                required: 'Name is required'
              })}
            />
            <Input
              label={`Email ${index + 1}`}
              type="email"
              {...register(`users.${index}.email` as const, {
                required: 'Email is required'
              })}
            />
          </div>
          {fields.length > 1 && (
            <Button
              type="button"
              color="danger"
              variant="light"
              onClick={() => remove(index)}
            >
              Remove
            </Button>
          )}
        </div>
      ))}
 
      <Button
        type="button"
        variant="bordered"
        onClick={() => append({ name: '', email: '' })}
      >
        Add User
      </Button>
 
      <Button type="submit" color="primary">
        Submit All
      </Button>
    </form>
  )
}

Conditional Fields

Show/hide fields based on conditions:

'use client'
 
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function ConditionalFields() {
  const [accountType, setAccountType] = useState<'personal' | 'business'>('personal')
  const { register, handleSubmit } = useForm()
 
  const onSubmit = (data: any) => {
    console.log('Submitted:', data)
  }
 
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
      <div className="flex gap-4">
        <Button
          type="button"
          color={accountType === 'personal' ? 'primary' : 'default'}
          onClick={() => setAccountType('personal')}
        >
          Personal
        </Button>
        <Button
          type="button"
          color={accountType === 'business' ? 'primary' : 'default'}
          onClick={() => setAccountType('business')}
        >
          Business
        </Button>
      </div>
 
      {accountType === 'personal' ? (
        <>
          <Input
            label="First Name"
            {...register('firstName', { required: true })}
          />
          <Input
            label="Last Name"
            {...register('lastName', { required: true })}
          />
        </>
      ) : (
        <>
          <Input
            label="Company Name"
            {...register('companyName', { required: true })}
          />
          <Input
            label="Tax ID"
            {...register('taxId', { required: true })}
          />
          <Input
            label="Number of Employees"
            type="number"
            {...register('employees', { required: true })}
          />
        </>
      )}
 
      <Input
        label="Email"
        type="email"
        {...register('email', { required: true })}
      />
 
      <Button type="submit" color="primary">
        Create Account
      </Button>
    </form>
  )
}

Form Submission

Basic Submission

Handle form submission with loading states:

'use client'
 
import { useState } from 'react'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
 
export default function FormSubmission() {
  const [isLoading, setIsLoading] = useState(false)
  const [message, setMessage] = useState('')
 
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setIsLoading(true)
    setMessage('')
 
    try {
      const formData = new FormData(e.currentTarget)
      const data = Object.fromEntries(formData)
 
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000))
 
      // In real app, make API request here
      // const response = await fetch('/api/submit', {
      //   method: 'POST',
      //   body: JSON.stringify(data)
      // })
 
      setMessage('Form submitted successfully!')
    } catch (error) {
      setMessage('Error submitting form. Please try again.')
    } finally {
      setIsLoading(false)
    }
  }
 
  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <Input label="Name" name="name" isRequired />
      <Input label="Email" name="email" type="email" isRequired />
 
      {message && (
        <div className={`p-3 rounded ${
          message.includes('Error')
            ? 'bg-danger-50 text-danger'
            : 'bg-success-50 text-success'
        }`}>
          {message}
        </div>
      )}
 
      <Button type="submit" color="primary" isLoading={isLoading}>
        {isLoading ? 'Submitting...' : 'Submit'}
      </Button>
    </form>
  )
}

Server Actions (Next.js)

Use Next.js Server Actions for form submission:

'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function submitForm(formData: FormData) {
  const data = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  }
 
  // Validate data
  if (!data.name || !data.email) {
    return { error: 'Name and email are required' }
  }
 
  // Save to database
  try {
    // await db.insert(data)
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Failed to submit form' }
  }
}
'use client'
 
import { useFormState } from 'react-dom'
import { Input } from '@flexi-ui/input'
import { Button } from '@flexi-ui/button'
import { submitForm } from './actions'
 
export default function ServerActionForm() {
  const [state, formAction] = useFormState(submitForm, null)
 
  return (
    <form action={formAction} className="flex flex-col gap-4">
      <Input label="Name" name="name" isRequired />
      <Input label="Email" name="email" type="email" isRequired />
      <Input label="Message" name="message" isRequired />
 
      {state?.error && (
        <div className="p-3 rounded bg-danger-50 text-danger">
          {state.error}
        </div>
      )}
 
      {state?.success && (
        <div className="p-3 rounded bg-success-50 text-success">
          Form submitted successfully!
        </div>
      )}
 
      <Button type="submit" color="primary">
        Submit
      </Button>
    </form>
  )
}

Best Practices

1. Provide Clear Feedback

Always give users feedback about their input:

<Input
  label="Email"
  type="email"
  description="We'll never share your email"
  errorMessage="Please enter a valid email"
  isRequired
/>

2. Use Appropriate Input Types

Use the right input type for better UX and validation:

{/* Email */}
<Input type="email" label="Email" />
 
{/* Phone */}
<Input type="tel" label="Phone" />
 
{/* Number */}
<Input type="number" label="Age" min={0} max={120} />
 
{/* Date */}
<Input type="date" label="Birth Date" />
 
{/* Password */}
<Input type="password" label="Password" />

3. Disable Submit While Loading

Prevent duplicate submissions:

<Button
  type="submit"
  isLoading={isSubmitting}
  isDisabled={isSubmitting}
>
  {isSubmitting ? 'Submitting...' : 'Submit'}
</Button>

4. Validate on Blur

Show errors after user finishes typing:

const formik = useFormik({
  validateOnChange: false,
  validateOnBlur: true,
  // ...
})

5. Reset Form After Submission

Clear form after successful submission:

const { reset } = useForm()
 
const onSubmit = async (data) => {
  await submitData(data)
  reset() // Clear form
}

Troubleshooting

Input Not Updating

Problem: Controlled input value not updating.

Solution: Ensure you're updating state correctly:

// Correct
<Input
  value={value}
  onChange={(e) => setValue(e.target.value)}
/>
 
// Incorrect - missing onChange
<Input value={value} />

Validation Not Working

Problem: Error messages not showing.

Solution: Make sure to pass both isInvalid and errorMessage:

<Input
  isInvalid={!!errors.email}
  errorMessage={errors.email?.message}
/>

Form Submitting on Enter

Problem: Form submits when pressing Enter in any input.

Solution: This is default HTML behavior. To prevent it:

<form onSubmit={handleSubmit} onKeyDown={(e) => {
  if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
    e.preventDefault()
  }
}}>

Next Steps

Now that you understand forms, explore: