# Show Errors Where the Action Happens

**MUST** · **ID:** `forms-ibelick-error-placement` · **Category:** forms
**Source:** [@Ibelick](https://www.ui-skills.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/forms-ibelick-error-placement

> MUST: Show errors inline next to where the action happens. Disconnected toasts or top-of-form error summaries force users to hunt for problems.

Put each error next to the thing that caused it: field errors on their field, submit errors by the submit button

From the Interaction constraints in ibelick's baseline-ui skill. "Where the action happens" is deliberately general and resolves per error: a validation error on the email field happened at the field, so it belongs under the field (wired with aria-describedby and aria-invalid); a submit failure — the request was rejected or the network died — happened at the button, so it belongs by the button. The anti-pattern is a detached error: a summary parked at the top of the page, or a toast that fires and vanishes, leaving nothing next to the control the user was actually touching. Read this as the placement rule that generalizes forms-error-placement (Vercel: "Errors inline next to fields"), not as a competitor to it — Vercel names the field case, ibelick names the whole family.

## Bad — do not do this

`forms-ibelick-error-placement-bad`

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

export function IbelickErrorPlacementBad() {
  const [submitted, setSubmitted] = useState(false);

  return (
    <div className="space-y-4">
      {/* Every error is parked here: far from the fields that failed AND far from the button that failed. */}
      {submitted && (
        <div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg" role="alert">
          <p className="text-sm font-medium text-destructive">Please fix the following:</p>
          <ul className="mt-1 text-xs text-destructive list-disc list-inside">
            <li>Invalid email format</li>
            <li>Password must be at least 8 characters</li>
            <li>Could not reach the server</li>
          </ul>
        </div>
      )}

      <form onSubmit={(e) => { e.preventDefault(); setSubmitted(true); }} className="space-y-4">
        <div>
          <label className="text-sm font-medium" htmlFor="bad-email">Email</label>
          <input
            id="bad-email"
            type="email"
            className={`w-full mt-1 px-3 py-2 border rounded-lg ${submitted ? 'border-destructive' : ''}`}
            placeholder="you@example.com"
          />
        </div>

        <div>
          <label className="text-sm font-medium" htmlFor="bad-password">Password</label>
          <input
            id="bad-password"
            type="password"
            className={`w-full mt-1 px-3 py-2 border rounded-lg ${submitted ? 'border-destructive' : ''}`}
            placeholder="••••••••"
          />
        </div>

        <button
          type="submit"
          className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg"
        >
          Sign Up
        </button>
      </form>
      <p className="text-xs text-destructive">
        No error is where its action happened: the field errors are detached from their fields (no
        aria-describedby, no message under the input) and the submit failure is nowhere near the button.
      </p>
    </div>
  );
}
```

## Good — do this

`forms-ibelick-error-placement-good`

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

export function IbelickErrorPlacementGood() {
  const [submitted, setSubmitted] = useState(false);

  return (
    <div className="space-y-4">
      <form onSubmit={(e) => { e.preventDefault(); setSubmitted(true); }} className="space-y-4">
        {/* Field error -> next to the field. That is where that action happened. */}
        <div>
          <label className="text-sm font-medium" htmlFor="good-email">Email</label>
          <input
            id="good-email"
            type="email"
            className={`w-full mt-1 px-3 py-2 border rounded-lg ${submitted ? 'border-destructive' : ''}`}
            placeholder="you@example.com"
            aria-invalid={submitted || undefined}
            aria-describedby={submitted ? 'good-email-error' : undefined}
          />
          {submitted && (
            <p id="good-email-error" className="text-xs text-destructive mt-1">
              Invalid email format
            </p>
          )}
        </div>

        <div>
          <label className="text-sm font-medium" htmlFor="good-password">Password</label>
          <input
            id="good-password"
            type="password"
            className={`w-full mt-1 px-3 py-2 border rounded-lg ${submitted ? 'border-destructive' : ''}`}
            placeholder="••••••••"
            aria-invalid={submitted || undefined}
            aria-describedby={submitted ? 'good-password-error' : undefined}
          />
          {submitted && (
            <p id="good-password-error" className="text-xs text-destructive mt-1">
              Password must be at least 8 characters
            </p>
          )}
        </div>

        {/* Submit-level error (the request failed) -> next to the button. That is where THAT action happened. */}
        {submitted && (
          <p className="text-sm text-destructive" role="alert">
            Could not reach the server. Nothing was saved — try again.
          </p>
        )}

        <button
          type="submit"
          className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg"
        >
          Sign Up
        </button>
      </form>
      <p className="text-xs text-success">
        Each error sits next to where its action happened: field errors on their fields (wired with
        aria-describedby), the submit failure by the button.
      </p>
    </div>
  );
}
```

## References

- [baseline-ui (skill source)](https://github.com/ibelick/ui-skills/blob/main/skills/baseline-ui/SKILL.md)
- [Form Error Handling](https://www.nngroup.com/articles/errors-forms-design-guidelines/)
