Skip to content
form-design logo

Form Design & Validation

form-design

Build accessible, user-friendly forms with validation. Covers react-hook-form, Zod schemas, error handling UX, multi-step forms, input patterns, and form accessibility. Use for registration forms, checkout flows, data entry, and user input.

SKILL.md

Full skill instructions

Form Design & Validation

Create accessible, validated forms with excellent user experience.

Instructions

  1. Use proper labels - Every input needs an associated label
  2. Validate on blur and submit - Immediate feedback without being intrusive
  3. Show clear error messages - Specific, actionable guidance
  4. Group related fields - Use fieldsets for logical groupings
  5. Support keyboard navigation - Tab order, Enter to submit

React Hook Form + Zod (Recommended Stack)

Setup

npm install react-hook-form zod @hookform/​resolvers

Basic Form

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/​resolvers/​zod';
import { z } from 'zod';

// 1. Define schema
const signupSchema = z.object({
  email: z
    .string()
    .min(1, 'Email is required')
    .email('Please enter a valid email'),
  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 SignupForm = z.infer<typeof signupSchema>;

// 2. Create form
function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<SignupForm>({
    resolver: zodResolver(signupSchema),
  });

  const onSubmit = async (data: SignupForm) => {
    try {
      await api.signup(data);
      // Handle success
    } catch (error) {
      // Handle error
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <FormField
        label="Email"
        type="email"
        error={errors.email?.message}
        {...register('email')}
      />

      <FormField
        label="Password"
        type="password"
        error={errors.password?.message}
        {...register('password')}
      />

      <FormField
        label="Confirm Password"
        type="password"
        error={errors.confirmPassword?.message}
        {...register('confirmPassword')}
      />

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Creating account...' : 'Sign Up'}
      </​button>
    </​form>
  );
}

Reusable Form Field Component

import { forwardRef } from 'react';

interface FormFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
  hint?: string;
}

export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
  ({ label, error, hint, id, type = 'text', ...props }, ref) => {
    const inputId = id || label.toLowerCase().replace(/\s+/​g, '-');
    const errorId = `${inputId}-error`;
    const hintId = `${inputId}-hint`;

    return (
      <div className="space-y-1.5">
        <label
          htmlFor={inputId}
          className="block text-sm font-medium text-gray-700 dark:text-gray-300"
        >
          {label}
          {props.required && <span className="text-red-500 ml-1">*</​span>}
        </​label>

        <input
          ref={ref}
          id={inputId}
          type={type}
          className={`
            w-full px-3 py-2 rounded-lg border transition-colors
            ${error
              ? 'border-red-500 focus:ring-red-500 focus:border-red-500'
              : 'border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500'
            }
            bg-white dark:bg-gray-800
            text-gray-900 dark:text-gray-100
            placeholder-gray-400
            focus:outline-none focus:ring-2
            disabled:bg-gray-100 disabled:cursor-not-allowed
          `}
          aria-invalid={!!error}
          aria-describedby={
            error ? errorId : hint ? hintId : undefined
          }
          {...props}
        />

        {hint && !error && (
          <p id={hintId} className="text-sm text-gray-500">
            {hint}
          </​p>
        )}

        {error && (
          <p id={errorId} className="text-sm text-red-600 flex items-center gap-1" role="alert">
            <ExclamationCircleIcon className="w-4 h-4" />
            {error}
          </​p>
        )}
      </​div>
    );
  }
);

Common Form Patterns

Select/​Dropdown

interface SelectFieldProps {
  label: string;
  options: { value: string; label: string }[];
  error?: string;
  placeholder?: string;
}

export const SelectField = forwardRef<HTMLSelectElement, SelectFieldProps>(
  ({ label, options, error, placeholder, ...props }, ref) => {
    const id = label.toLowerCase().replace(/\s+/​g, '-');

    return (
      <div className="space-y-1.5">
        <label htmlFor={id} className="block text-sm font-medium text-gray-700">
          {label}
        </​label>
        <select
          ref={ref}
          id={id}
          className={`
            w-full px-3 py-2 rounded-lg border
            ${error ? 'border-red-500' : 'border-gray-300'}
            bg-white focus:ring-2 focus:ring-blue-500
          `}
          aria-invalid={!!error}
          {...props}
        >
          {placeholder && (
            <option value="" disabled>
              {placeholder}
            </​option>
          )}
          {options.map(opt => (
            <option key={opt.value} value={opt.value}>
              {opt.label}
            </​option>
          ))}
        </​select>
        {error && <p className="text-sm text-red-600">{error}</​p>}
      </​div>
    );
  }
);

Checkbox Group

interface CheckboxGroupProps {
  label: string;
  options: { value: string; label: string }[];
  value: string[];
  onChange: (value: string[]) => void;
  error?: string;
}

function CheckboxGroup({ label, options, value, onChange, error }: CheckboxGroupProps) {
  const handleChange = (optionValue: string, checked: boolean) => {
    if (checked) {
      onChange([...value, optionValue]);
    } else {
      onChange(value.filter(v => v !== optionValue));
    }
  };

  return (
    <fieldset>
      <legend className="text-sm font-medium text-gray-700 mb-2">{label}</​legend>
      <div className="space-y-2">
        {options.map(opt => (
          <label key={opt.value} className="flex items-center gap-2 cursor-pointer">
            <input
              type="checkbox"
              checked={value.includes(opt.value)}
              onChange={(e) => handleChange(opt.value, e.target.checked)}
              className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
            />
            <span className="text-gray-700">{opt.label}</​span>
          </​label>
        ))}
      </​div>
      {error && <p className="text-sm text-red-600 mt-1">{error}</​p>}
    </​fieldset>
  );
}

Radio Group

interface RadioGroupProps {
  label: string;
  options: { value: string; label: string; description?: string }[];
  value: string;
  onChange: (value: string) => void;
  error?: string;
}

function RadioGroup({ label, options, value, onChange, error }: RadioGroupProps) {
  return (
    <fieldset>
      <legend className="text-sm font-medium text-gray-700 mb-3">{label}</​legend>
      <div className="space-y-3">
        {options.map(opt => (
          <label
            key={opt.value}
            className={`
              flex items-start gap-3 p-4 rounded-lg border cursor-pointer
              ${value === opt.value
                ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500'
                : 'border-gray-200 hover:border-gray-300'
              }
            `}
          >
            <input
              type="radio"
              name={label}
              value={opt.value}
              checked={value === opt.value}
              onChange={() => onChange(opt.value)}
              className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-blue-500"
            />
            <div>
              <span className="font-medium text-gray-900">{opt.label}</​span>
              {opt.description && (
                <p className="text-sm text-gray-500">{opt.description}</​p>
              )}
            </​div>
          </​label>
        ))}
      </​div>
      {error && <p className="text-sm text-red-600 mt-2">{error}</​p>}
    </​fieldset>
  );
}

Multi-Step Form

import { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';

const steps = [
  { id: 'account', title: 'Account', component: AccountStep },
  { id: 'profile', title: 'Profile', component: ProfileStep },
  { id: 'preferences', title: 'Preferences', component: PreferencesStep },
];

function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(0);
  const methods = useForm({ mode: 'onChange' });

  const CurrentStepComponent = steps[currentStep].component;
  const isLastStep = currentStep === steps.length - 1;

  const next = async () => {
    const isValid = await methods.trigger(); // Validate current step
    if (isValid && !isLastStep) {
      setCurrentStep(prev => prev + 1);
    }
  };

  const back = () => {
    if (currentStep > 0) {
      setCurrentStep(prev => prev - 1);
    }
  };

  const onSubmit = async (data: FormData) => {
    await api.submit(data);
  };

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        {/​* Progress indicator */​}
        <nav aria-label="Progress" className="mb-8">
          <ol className="flex items-center">
            {steps.map((step, index) => (
              <li key={step.id} className="flex items-center">
                <span className={`
                  w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium
                  ${index < currentStep
                    ? 'bg-blue-600 text-white'
                    : index === currentStep
                    ? 'border-2 border-blue-600 text-blue-600'
                    : 'border-2 border-gray-300 text-gray-500'
                  }
                `}>
                  {index < currentStep ? '✓' : index + 1}
                </​span>
                <span className="ml-2 text-sm font-medium text-gray-900">
                  {step.title}
                </​span>
                {index < steps.length - 1 && (
                  <div className="w-12 h-0.5 mx-4 bg-gray-200" />
                )}
              </​li>
            ))}
          </​ol>
        </​nav>

        {/​* Current step */​}
        <CurrentStepComponent />

        {/​* Navigation */​}
        <div className="flex justify-between mt-8">
          <button
            type="button"
            onClick={back}
            disabled={currentStep === 0}
            className="px-4 py-2 border rounded-lg disabled:opacity-50"
          >
            Back
          </​button>

          {isLastStep ? (
            <button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg">
              Submit
            </​button>
          ) : (
            <button type="button" onClick={next} className="px-4 py-2 bg-blue-600 text-white rounded-lg">
              Continue
            </​button>
          )}
        </​div>
      </​form>
    </​FormProvider>
  );
}

Error Handling Patterns

Inline Errors

// Show error immediately below field
{error && (
  <p className="text-sm text-red-600 mt-1 flex items-center gap-1">
    <ExclamationCircleIcon className="w-4 h-4 flex-shrink-0" />
    {error}
  </​p>
)}

Error Summary

// Show all errors at top of form
function ErrorSummary({ errors }: { errors: Record<string, { message?: string }> }) {
  const errorList = Object.entries(errors).filter(([_, v]) => v.message);

  if (errorList.length === 0) return null;

  return (
    <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6" role="alert">
      <h3 className="text-red-800 font-medium mb-2">
        Please fix the following errors:
      </​h3>
      <ul className="list-disc list-inside text-sm text-red-700 space-y-1">
        {errorList.map(([field, error]) => (
          <li key={field}>
            <a href={`#${field}`} className="underline">
              {error.message}
            </​a>
          </​li>
        ))}
      </​ul>
    </​div>
  );
}

Accessibility Checklist

  • Every input has a <label> with htmlFor
  • Required fields are marked (and announced)
  • Error messages are linked with aria-describedby
  • Invalid fields have aria-invalid="true"
  • Error messages use role="alert" for screen readers
  • Focus moves to first error on submit
  • Tab order is logical
  • Submit with Enter key works

Best Practices

  1. Don't disable submit - Show errors instead
  2. Validate on blur - Immediate but not intrusive
  3. Pre-fill when possible - Reduce user effort
  4. Show password requirements - Before they fail
  5. Confirm destructive actions - Double-check deletes
  6. Save progress - For long forms, use localStorage

When to Use

  • User registration and login forms
  • Checkout and payment flows
  • Settings and profile forms
  • Data entry applications
  • Contact and feedback forms

Notes

  • react-hook-form is the most performant React form library
  • Zod provides runtime validation + TypeScript types
  • Test forms with screen readers and keyboard-only
  • Consider form analytics to find drop-off points

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View