# Match Visual & Hit Targets

**MUST** · **ID:** `interactions-match-hit-targets` · **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-match-hit-targets

> MUST: Hit target ≥24px (mobile ≥44px) If visual <24px, expand hit area

If visual target is < 24px, expand hit target to at least 24px

Small interactive elements are hard to click or tap. While an icon might be 16px visually, its clickable area should be at least 24px on desktop and 44px on mobile. Use padding or pseudo-elements to expand the hit target without changing the visual appearance.

## Bad — do not do this

`interactions-match-hit-targets-bad`

```tsx
import { useState } from 'react';
import { X } from 'lucide-react';

export function MatchHitTargetsBad() {
  const [showArea, setShowArea] = useState(true);
  const [hits, setHits] = useState(0);

  return (
    <div className="w-full max-w-sm space-y-4">
      <label className="flex w-fit cursor-pointer items-center gap-2 text-xs text-muted-foreground select-none">
        <input
          type="checkbox"
          checked={showArea}
          onChange={(e) => setShowArea(e.target.checked)}
          className="accent-error"
        />
        Show hit area
      </label>

      <div className="flex items-center justify-between rounded-lg bg-muted p-4">
        <span className="text-sm">Notification message</span>
        <button
          onClick={() => setHits((n) => n + 1)}
          className={`rounded text-muted-foreground transition-colors hover:text-foreground ${
            showArea ? 'bg-error/20 ring-2 ring-inset ring-error' : ''
          }`}
          aria-label="Dismiss"
        >
          <X className="h-4 w-4" />
        </button>
      </div>

      <p className="text-xs text-error">
        {hits > 0
          ? `Clicked ${hits}× — but only if you landed on the bare 16px icon.`
          : 'The hit target is just the 16px icon. Toggle the box, then try to click it — easy to miss.'}
      </p>
    </div>
  );
}
```

## Good — do this

`interactions-match-hit-targets-good`

```tsx
import { useState } from 'react';
import { X } from 'lucide-react';

export function MatchHitTargetsGood() {
  const [showArea, setShowArea] = useState(true);
  const [hits, setHits] = useState(0);

  return (
    <div className="w-full max-w-sm space-y-4">
      <label className="flex w-fit cursor-pointer items-center gap-2 text-xs text-muted-foreground select-none">
        <input
          type="checkbox"
          checked={showArea}
          onChange={(e) => setShowArea(e.target.checked)}
          className="accent-success"
        />
        Show hit area
      </label>

      <div className="flex items-center justify-between rounded-lg bg-muted p-4">
        <span className="text-sm">Notification message</span>
        <button
          onClick={() => setHits((n) => n + 1)}
          className={`-m-2 rounded p-2 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring ${
            showArea ? 'bg-success/20 ring-2 ring-inset ring-success' : ''
          }`}
          aria-label="Dismiss"
        >
          <X className="h-4 w-4" />
        </button>
      </div>

      <p className="text-xs text-success">
        {hits > 0
          ? `Clicked ${hits}× — the 32px padded target catches every tap.`
          : 'Padding expands the hit target to 32px while the icon stays 16px. Toggle the box, then click anywhere inside it.'}
      </p>
    </div>
  );
}
```

## References

- [WCAG Target Size](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html)
- [Touch Target Sizes](https://www.nngroup.com/articles/touch-target-size/)
