form-design
Form Design & Development
Build accessible, user-friendly forms with proper validation and error handling.
Instructions
- Use react-hook-form - Performant form state management
- Validate with Zod - Type-safe schema validation
- Show errors inline - Near the relevant field
- Provide clear feedback - Success, error, and loading states
- Ensure accessibility - Labels, ARIA attributes, keyboard navigation
React Hook Form + Zod
Basic Form Setup
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
.regex(/[0-9]/, 'Password must contain a number'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type FormData = z.infer<typeof formSchema>;
export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = async (data: FormData) => {
try {
await createAccount(data);
} catch (error) {
// Handle API errors
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register('email')}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" role="alert">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register('password')}
aria-invalid={!!errors.password}
aria-describedby={errors.password ? 'password-error' : undefined}
/>
{errors.password && (
<p id="password-error" role="alert">{errors.password.message}</p>
)}
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
{...register('confirmPassword')}
aria-invalid={!!errors.confirmPassword}
/>
{errors.confirmPassword && (
<p role="alert">{errors.confirmPassword.message}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating account...' : 'Sign Up'}
</button>
</form>
);
}
Reusable Form Field Component
import { useFormContext } from 'react-hook-form';
interface FormFieldProps {
name: string;
label: string;
type?: string;
placeholder?: string;
hint?: string;
}
export function FormField({
name,
label,
type = 'text',
placeholder,
hint,
}: FormFieldProps) {
const {
register,
formState: { errors },
} = useFormContext();
const error = errors[name]?.message as string | undefined;
const inputId = `field-${name}`;
const errorId = `${inputId}-error`;
const hintId = `${inputId}-hint`;
return (
<div className="space-y-1">
<label
htmlFor={inputId}
className="block text-sm font-medium text-gray-700"
>
{label}
</label>
<input
id={inputId}
type={type}
placeholder={placeholder}
{...register(name)}
className={`
w-full px-3 py-2 border rounded-lg
${error ? 'border-red-500' : 'border-gray-300'}
focus:outline-none focus:ring-2
${error ? 'focus:ring-red-500' : 'focus:ring-blue-500'}
`}
aria-invalid={!!error}
aria-describedby={
error ? errorId : hint ? hintId : undefined
}
/>
{hint && !error && (
<p id={hintId} className="text-sm text-gray-500">
{hint}
</p>
)}
{error && (
<p id={errorId} className="text-sm text-red-600" role="alert">
{error}
</p>
)}
</div>
);
}
Multi-Step Forms
import { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Step schemas
const step1Schema = z.object({
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email'),
});
const step2Schema = z.object({
address: z.string().min(1, 'Address is required'),
city: z.string().min(1, 'City is required'),
zipCode: z.string().regex(/^\d{5}$/, 'Invalid ZIP code'),
});
const step3Schema = z.object({
cardNumber: z.string().regex(/^\d{16}$/, 'Invalid card number'),
expiry: z.string().regex(/^\d{2}\/\d{2}$/, 'Format: MM/YY'),
cvv: z.string().regex(/^\d{3,4}$/, 'Invalid CVV'),
});
const fullSchema = step1Schema.merge(step2Schema).merge(step3Schema);
type FormData = z.infer<typeof fullSchema>;
const steps = [
{ schema: step1Schema, title: 'Personal Info' },
{ schema: step2Schema, title: 'Address' },
{ schema: step3Schema, title: 'Payment' },
];
export function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(0);
const methods = useForm<FormData>({
resolver: zodResolver(fullSchema),
mode: 'onChange',
});
const { trigger, handleSubmit } = methods;
const goToNextStep = async () => {
const currentSchema = steps[currentStep].schema;
const fields = Object.keys(currentSchema.shape) as (keyof FormData)[];
const isValid = await trigger(fields);
if (isValid) {
setCurrentStep((prev) => prev + 1);
}
};
const goToPreviousStep = () => {
setCurrentStep((prev) => prev - 1);
};
const onSubmit = async (data: FormData) => {
console.log('Form submitted:', data);
};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Progress indicator */}
<div className="flex justify-between mb-8">
{steps.map((step, index) => (
<div
key={step.title}
className={`flex items-center ${
index <= currentStep ? 'text-blue-600' : 'text-gray-400'
}`}
>
<span className={`
w-8 h-8 rounded-full flex items-center justify-center
${index <= currentStep ? 'bg-blue-600 text-white' : 'bg-gray-200'}
`}>
{index + 1}
</span>
<span className="ml-2 text-sm">{step.title}</span>
</div>
))}
</div>
{/* Step content */}
{currentStep === 0 && <Step1 />}
{currentStep === 1 && <Step2 />}
{currentStep === 2 && <Step3 />}
{/* Navigation */}
<div className="flex justify-between mt-8">
<button
type="button"
onClick={goToPreviousStep}
disabled={currentStep === 0}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Previous
</button>
{currentStep < steps.length - 1 ? (
<button
type="button"
onClick={goToNextStep}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Next
</button>
) : (
<button
type="submit"
className="px-4 py-2 bg-green-600 text-white rounded"
>
Submit
</button>
)}
</div>
</form>
</FormProvider>
);
}
Accessible Form Patterns
Required Field Indicators
<label htmlFor="email">
Email
<span className="text-red-500" aria-hidden="true">*</span>
<span className="sr-only">(required)</span>
</label>
Error Announcements
// Live region for form errors
<div
role="alert"
aria-live="polite"
className="sr-only"
>
{Object.keys(errors).length > 0 && (
`Form has ${Object.keys(errors).length} errors. Please correct them.`
)}
</div>
Focus Management
const { setFocus } = useForm();
// Focus first error field on submit failure
const onInvalid = () => {
const firstErrorField = Object.keys(errors)[0];
if (firstErrorField) {
setFocus(firstErrorField as keyof FormData);
}
};
<form onSubmit={handleSubmit(onSubmit, onInvalid)}>
Input Patterns
Phone Number Input
const phoneSchema = z.string().regex(
/^\+?[1-9]\d{1,14}$/,
'Enter a valid phone number'
);
<input
type="tel"
inputMode="tel"
autoComplete="tel"
placeholder="+1 (555) 123-4567"
/>
Date Input
const dateSchema = z.string().refine(
(date) => !isNaN(Date.parse(date)),
'Enter a valid date'
);
<input
type="date"
min={new Date().toISOString().split('T')[0]}
autoComplete="bday"
/>
Currency Input
const currencySchema = z
.string()
.transform((val) => parseFloat(val.replace(/[^0-9.]/g, '')))
.refine((val) => !isNaN(val) && val >= 0, 'Enter a valid amount');
<input
type="text"
inputMode="decimal"
placeholder="$0.00"
onChange={(e) => {
const value = e.target.value.replace(/[^0-9.]/g, '');
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(parseFloat(value) || 0);
e.target.value = formatted;
}}
/>
Best Practices
- Validate on blur - Show errors after user leaves field
- Clear errors on focus - Give users a fresh start
- Use native inputs - Better mobile experience
- Show password requirements - Before user types
- Disable submit while invalid - Prevent frustration
- Save progress - For multi-step forms
- Handle server errors - Display API validation errors
When to Use
- User registration and login
- Checkout and payment flows
- Profile and settings pages
- Data entry applications
- Survey and feedback forms
Notes
- Always use
noValidateon forms to control validation - Use
inputModefor mobile keyboards - Test with keyboard-only navigation
- Consider autofill attributes for better UX
More from housegarofalo/claude-code-base
mqtt-iot
Configure MQTT brokers (Mosquitto, EMQX) for IoT messaging, device communication, and smart home integration. Manage topics, QoS levels, authentication, and bridging. Use when setting up IoT messaging, smart home communication, or device-to-cloud connectivity. (project)
22devops-engineer-agent
Infrastructure and DevOps specialist. Manages Docker, Kubernetes, CI/CD pipelines, and cloud deployments. Expert in GitHub Actions, Azure DevOps, Terraform, and container orchestration. Use for deployment automation, infrastructure setup, or CI/CD optimization.
6postgresql
Design, optimize, and manage PostgreSQL databases. Covers indexing, pgvector for AI embeddings, JSON operations, full-text search, and query optimization. Use when working with PostgreSQL, database design, or building data-intensive applications.
6home-assistant
Ultimate Home Assistant skill - complete administration, wireless protocols (Zigbee/ZHA/Z2M, Z-Wave JS, Thread, Matter), ESPHome device building, advanced troubleshooting, performance optimization, security hardening, custom integration development, and professional dashboard design. Covers configuration, REST API, automation debugging, database optimization, SSL/TLS, Jinja2 templating, and HACS custom cards. Use for any HA task.
6testing
Comprehensive testing skill covering unit, integration, and E2E testing with pytest, Jest, Cypress, and Playwright. Use for writing tests, improving coverage, debugging test failures, and setting up testing infrastructure.
5react-typescript
Build modern React applications with TypeScript. Covers React 18+ patterns, hooks, component architecture, state management (Zustand, Redux Toolkit), server components, and best practices. Use for React development, TypeScript integration, component design, and frontend architecture.
5