# Design Forgiving Interactions

**MUST** · **ID:** `interactions-forgiving-design` · **Category:** interactions
**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/interactions-forgiving-design

> MUST: Design forgiving interactions (generous targets, clear affordances; avoid finickiness)

Controls minimize finickiness with generous hit targets and clear affordances

Make interactive elements easy to use by providing generous hit targets, clear visual feedback, and forgiving interaction patterns. A prediction cone is the triangular region between the cursor and an open submenu: while the pointer moves inside that triangle, keep the submenu open even though the cursor has technically left the parent item. Without it, cutting the corner toward the submenu closes it out from under the user.

## Bad — do not do this

`interactions-forgiving-design-bad`

```tsx
import { useState } from 'react';
import { Minus, Plus } from 'lucide-react';

export function ForgivingDesignBad() {
  const [volume, setVolume] = useState(50);

  const step = (delta: number) =>
    setVolume((v) => Math.min(100, Math.max(0, v + delta)));

  return (
    <div className="w-full max-w-sm">
      <div className="rounded-lg border border-border bg-card p-4">
        <div className="mb-4 flex items-center gap-3">
          <span className="text-sm text-foreground">Volume</span>
          <input
            type="range"
            min="0"
            max="100"
            step="10"
            value={volume}
            onChange={(e) => setVolume(Number(e.target.value))}
            className="h-0.5 flex-1 cursor-pointer accent-primary"
            aria-label="Volume"
          />
          <span className="w-8 text-right text-sm tabular-nums text-muted-foreground">
            {volume}
          </span>
        </div>
        <div className="mb-4 flex items-center gap-4">
          <button
            onClick={() => step(-10)}
            aria-label="Decrease volume"
            className="flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground"
          >
            <Minus className="h-3 w-3" />
          </button>
          <button
            onClick={() => step(10)}
            aria-label="Increase volume"
            className="flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground"
          >
            <Plus className="h-3 w-3" />
          </button>
        </div>
        <p className="text-xs text-muted-foreground">
          Small hit targets (24px) are hard to tap on mobile. The slider track is thin and difficult to grab.
        </p>
      </div>
      <p className="mt-4 text-xs text-error">
        Tiny hit targets require precise tapping
      </p>
    </div>
  );
}
```

## Good — do this

`interactions-forgiving-design-good`

```tsx
import { useState } from 'react';
import { Minus, Plus } from 'lucide-react';

export function ForgivingDesignGood() {
  const [volume, setVolume] = useState(50);

  const step = (delta: number) =>
    setVolume((v) => Math.min(100, Math.max(0, v + delta)));

  return (
    <div className="w-full max-w-sm">
      <div className="rounded-lg border border-border bg-card p-4">
        <div className="mb-4 flex items-center gap-3">
          <span className="text-sm text-foreground">Volume</span>
          <input
            type="range"
            min="0"
            max="100"
            step="10"
            value={volume}
            onChange={(e) => setVolume(Number(e.target.value))}
            className="h-2 flex-1 cursor-pointer accent-primary"
            aria-label="Volume"
          />
          <span className="w-8 text-right text-sm tabular-nums text-muted-foreground">
            {volume}
          </span>
        </div>
        <div className="mb-4 flex items-center gap-2">
          <button
            onClick={() => step(-10)}
            aria-label="Decrease volume"
            className="flex h-11 w-11 items-center justify-center rounded-lg border border-border bg-background text-foreground transition-colors hover:bg-muted focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          >
            <Minus className="h-5 w-5" />
          </button>
          <button
            onClick={() => step(10)}
            aria-label="Increase volume"
            className="flex h-11 w-11 items-center justify-center rounded-lg border border-border bg-background text-foreground transition-colors hover:bg-muted focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          >
            <Plus className="h-5 w-5" />
          </button>
        </div>
        <p className="text-xs text-muted-foreground">
          44px hit targets are easy to tap. Generous padding and clear hover states provide feedback.
        </p>
      </div>
      <p className="mt-4 text-xs text-success">
        44px+ hit targets with clear affordances
      </p>
    </div>
  );
}
```

## References

- [Fitts's Law](https://www.nngroup.com/articles/fitts-law/)
