# Error Placement

**MUST** · **ID:** `forms-error-placement` · **Category:** forms
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/forms-error-placement

> MUST: Errors inline next to fields; on submit, focus first error

Show errors next to their fields and focus the first error on submit

Errors should appear immediately adjacent to the field they relate to. When a form is submitted with errors, automatically focus the first problematic field so users can start fixing issues immediately. This is especially important for long forms.

## Bad — do not do this

`forms-error-placement-bad`

```tsx
import { useState } from 'react';

export function ErrorPlacementBad() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!name || !email.includes('@')) {
      setError('Please fix the errors in the form');
    } else {
      setError('');
      alert('Submitted!');
    }
  };

  return (
    <div className="w-full max-w-sm">
      {error && (
        <div className="mb-4 p-3 bg-error/10 border border-error/20 rounded-lg text-sm text-error">
          {error}
        </div>
      )}
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label htmlFor="bad-error-name" className="block text-sm font-medium text-foreground mb-1">
            Name
          </label>
          <input
            id="bad-error-name"
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="w-full px-3 py-2 border border-border rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          />
        </div>
        <div>
          <label htmlFor="bad-error-email" className="block text-sm font-medium text-foreground mb-1">
            Email
          </label>
          <input
            id="bad-error-email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="w-full px-3 py-2 border border-border rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          />
        </div>
        <button
          type="submit"
          className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
        >
          Submit
        </button>
      </form>
      <p className="text-xs text-error mt-4">
        Error at top is vague. No focus management.
      </p>
    </div>
  );
}
```

## Good — do this

`forms-error-placement-good`

```tsx
import { useState, useRef } from 'react';

export function ErrorPlacementGood() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [errors, setErrors] = useState<{ name?: string; email?: string }>({});
  const nameRef = useRef<HTMLInputElement>(null);
  const emailRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const newErrors: { name?: string; email?: string } = {};

    if (!name.trim()) {
      newErrors.name = 'Name is required';
    }
    if (!email.includes('@')) {
      newErrors.email = 'Please enter a valid email address';
    }

    setErrors(newErrors);

    if (Object.keys(newErrors).length > 0) {
      if (newErrors.name) {
        nameRef.current?.focus();
      } else if (newErrors.email) {
        emailRef.current?.focus();
      }
    } else {
      alert('Submitted!');
    }
  };

  return (
    <div className="w-full max-w-sm">
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label htmlFor="good-error-name" className="block text-sm font-medium text-foreground mb-1">
            Name
          </label>
          <input
            ref={nameRef}
            id="good-error-name"
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className={`w-full px-3 py-2 border rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring ${
              errors.name ? 'border-error/50' : 'border-border'
            }`}
          />
          {errors.name && (
            <p className="text-xs text-error mt-1">{errors.name}</p>
          )}
        </div>
        <div>
          <label htmlFor="good-error-email" className="block text-sm font-medium text-foreground mb-1">
            Email
          </label>
          <input
            ref={emailRef}
            id="good-error-email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className={`w-full px-3 py-2 border rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring ${
              errors.email ? 'border-error/50' : 'border-border'
            }`}
          />
          {errors.email && (
            <p className="text-xs text-error mt-1">{errors.email}</p>
          )}
        </div>
        <button
          type="submit"
          className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
        >
          Submit
        </button>
      </form>
      <p className="text-xs text-success mt-4">
        Errors appear inline. First error gets focus.
      </p>
    </div>
  );
}
```

## References

- [WebAIM: Accessible Form Validation](https://webaim.org/techniques/formvalidation/)
