Skip to content

Form Component

A robust, accessible form component built on Radix UI primitives with comprehensive validation, error handling, and submission management. Designed for complex forms with real-time validation and seamless user experience.

  • ♿ Accessibility First: Built on Radix UI Form primitives with ARIA compliance
  • 🔍 Real-time Validation: Client-side validation with immediate feedback
  • 🚀 Advanced Submission: Custom handlers for validation, success, and error states
  • 🎯 Type Safety: Full TypeScript support with form data typing
  • 🔧 Flexible: Works with any form fields and validation libraries
  • ⚡ Performance: Optimized for large forms with efficient re-renders
  • 🌐 Progressive Enhancement: Works without JavaScript as fallback

A simple contact form demonstrating core functionality:

<Form onSubmit={(e) => {
e.preventDefault();
console.log('Form submitted!');
}}>
<div>
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
name="email"
required
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
required
rows="4"
placeholder="Tell us about your project..."
/>
</div>
<button type="submit">Send Message</button>
</Form>

Example showing custom validation, error handling, and success states:

const handleRegistration = async (formData) => {
try {
const response = await fetch('/api/register', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Registration failed');
}
return await response.json();
} catch (error) {
throw error;
}
};
<Form
onValidate={(formData) => {
const email = formData.get('email');
const password = formData.get('password');
// Client-side validation
if (!email?.includes('@')) return false;
if (!password || password.length < 8) return false;
return true;
}}
onFormSuccess={(formData) => {
console.log('Registration successful:', formData);
router.push('/welcome');
}}
onFormError={(error) => {
console.error('Registration error:', error);
setErrorMessage(error.message);
}}
>
{/* Form fields */}
</Form>

Demonstrating loading states and disabled form management:

const [isSubmitting, setIsSubmitting] = useState(false);
<Form
onSubmit={async (event) => {
event.preventDefault();
setIsSubmitting(true);
try {
const formData = new FormData(event.currentTarget);
await submitForm(formData);
} finally {
setIsSubmitting(false);
}
}}
>
{/* Form fields */}
<button
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</button>
</Form>

Props

NameTypeRequiredDefaultDescription
onSubmit((event: React.FormEvent<HTMLFormElement>) => void) | undefinedOptionalForm submission handler called after validation passes
clearOnSubmitboolean | undefinedOptionalWhether to clear form fields after successful submission
preventDefaultSubmissionboolean | undefinedOptionalWhether to prevent default browser form submission
onValidate((formData: FormData) => boolean | Promise<boolean>) | undefinedOptionalCustom validation handler called before submission
onFormError((error: Error) => void) | undefinedOptionalError handler for form submission errors
onFormSuccess((formData: FormData) => void) | undefinedOptionalSuccess handler called after successful form submission
PropTypeDefaultDescription
onSubmit(event: FormEvent) => void-Standard form submission handler
onValidate(formData: FormData) => boolean | Promise<boolean>-Custom validation function
onFormSuccess(formData: FormData) => void-Called on successful form submission
onFormError(error: Error) => void-Called when form submission fails
clearOnSubmitbooleanfalseClear form after successful submission
preventDefaultSubmissionbooleanfalsePrevent default form submission behavior
childrenReact.ReactNode-Form content and fields
classNamestring-Additional CSS classes

The Form component provides several ways to handle form data:

<Form onValidate={(formData) => {
const email = formData.get('email') as string;
const age = Number(formData.get('age'));
return email.includes('@') && age >= 18;
}}>
{/* Form fields */}
</Form>
const formDataToObject = (formData) => {
return Object.fromEntries(formData.entries());
};
<Form onFormSuccess={(formData) => {
const data = formDataToObject(formData);
console.log(data); // { email: 'user@example.com', name: 'John' }
}}>
{/* Form fields */}
</Form>
const validateForm = (formData) => {
const email = formData.get('email');
const password = formData.get('password');
const confirmPassword = formData.get('confirmPassword');
// Email validation
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return false;
}
// Password validation
if (!password || password.length < 8) {
return false;
}
// Password confirmation
if (password !== confirmPassword) {
return false;
}
return true;
};
<Form onValidate={validateForm}>
{/* Form fields */}
</Form>
const validateAsync = async (formData) => {
const username = formData.get('username');
// Check username availability
try {
const response = await fetch(`/api/check-username/${username}`);
const { available } = await response.json();
return available;
} catch (error) {
console.error('Validation error:', error);
return false;
}
};
<Form onValidate={validateAsync}>
{/* Form fields */}
</Form>
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().min(18)
});
const validateWithZod = (formData) => {
try {
const data = {
email: formData.get('email'),
password: formData.get('password'),
age: Number(formData.get('age'))
};
schema.parse(data);
return true;
} catch (error) {
console.error('Validation errors:', error.errors);
return false;
}
};
<Form onValidate={validateWithZod}>
{/* Form fields */}
</Form>

The Form component handles various error scenarios:

<Form
onFormError={(error) => {
if (error.name === 'ValidationError') {
setValidationErrors(error.details);
} else if (error.name === 'NetworkError') {
setNetworkError('Please check your connection');
} else {
setGeneralError('Something went wrong');
}
}}
>
{/* Form fields */}
</Form>
const [submitAttempts, setSubmitAttempts] = useState(0);
const maxAttempts = 3;
<Form
onFormError={(error) => {
setSubmitAttempts(prev => prev + 1);
if (submitAttempts < maxAttempts) {
// Show retry option
setShowRetry(true);
} else {
// Disable form after max attempts
setFormDisabled(true);
}
}}
>
{/* Form fields */}
</Form>

The Form component prioritizes accessibility and follows ARIA best practices:

  • Proper form landmarks and structure
  • Field labels associated with inputs
  • Error messages announced to screen readers
  • Success confirmations communicated
  • Tab order respects visual layout
  • Enter key submits forms appropriately
  • Escape key can cancel submissions (custom)
  • Arrow keys for radio/checkbox groups
const formRef = useRef(null);
<Form
ref={formRef}
onFormError={(error) => {
// Focus first invalid field
const firstInvalid = formRef.current?.querySelector(':invalid');
firstInvalid?.focus();
}}
>
{/* Form fields */}
</Form>
// ✅ Good: Clear labels and structure
<Form>
<fieldset>
<legend>Contact Information</legend>
<div>
<label htmlFor="name">Full Name *</label>
<input
id="name"
name="name"
required
aria-describedby="name-help"
/>
<div id="name-help">Enter your full legal name</div>
</div>
</fieldset>
</Form>
// ❌ Avoid: Missing labels and unclear structure
<Form>
<input placeholder="Name" />
<input placeholder="Email" />
</Form>

For forms with many fields, consider performance optimizations:

import { memo } from 'react';
const FormField = memo(({ label, name, type }) => (
<div>
<label htmlFor={name}>{label}</label>
<input id={name} name={name} type={type} />
</div>
));
<Form>
{fields.map(field => (
<FormField key={field.name} {...field} />
))}
</Form>
import { useDebouncedCallback } from 'use-debounce';
const debouncedValidation = useDebouncedCallback(
(formData) => validateForm(formData),
300
);
<Form onValidate={debouncedValidation}>
{/* Form fields */}
</Form>
import { useForm } from 'react-hook-form';
const MyForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: true })} />
{errors.email && <span>Email is required</span>}
</Form>
);
};
import { Formik } from 'formik';
<Formik
initialValues={{ email: '', password: '' }}
onSubmit={handleSubmit}
>
{({ handleSubmit: formikSubmit }) => (
<Form onSubmit={formikSubmit}>
{/* Form fields */}
</Form>
)}
</Formik>

View the source code for this component:

  • v1.0.0: Initial form component with Radix UI integration
  • v1.1.0: Added validation handlers and error management
  • v1.2.0: Enhanced accessibility and TypeScript support
  • v1.3.0: Added success/error callbacks and performance optimizations

For detailed TypeScript definitions and additional API information, see the API Documentation.