Interaction details
Where does focus go when the dialog closes? Can you hit that icon on a phone?
## Focus management: trap and restore
Dialog opens: tab cycles _within_ the overlay. Dialog closes: focus returns to the trigger.
```tsx
<Dialog open onClose={close}>
<Dialog.Content>
<input autoFocus />
<button>Save</button>
<button onClick={close}>Cancel</button>
</Dialog.Content>
</Dialog>
```
Native `<dialog>` handles trapping. Radix, Aria, Headless UI handle trapping and restoration.
Other focus moments:
- **On open.** Focus the first form field, not the close button.
- **On validation failure.** Focus the first invalid field.
- **After deletion.** Focus the next item. Never drop to `<body>`.
## Hit targets: 24px desktop, 44px mobile
- **24 x 24px** desktop (WCAG 2.2 minimum).
- **44 x 44px** touch (Apple HIG).
These are _hit area_, not visual size. A 16px icon can have a 44px tap target:
```css
.icon-button {
padding: 14px;
/* or expand with a pseudo-element: */
}
.icon-button::after {
content: "";
position: absolute;
inset: -14px;
}
```
Adjacent targets need 8px minimum between hit areas.
## Hover gates
Touch devices fire hover on tap. Gate hover behind a media query:
```css
button {
background: var(--rest);
}
@media (hover: hover) and (pointer: fine) {
button:hover {
background: var(--hover);
}
}
```
Use `:active` for press feedback and `:focus-visible` for keyboard.
## `touch-action` for gestures
```css
.swipe-panel {
touch-action: pan-y; /* browser handles vertical scroll; you handle horizontal */
}
```
- `pan-y`: vertical scroll only.
- `pan-x`: horizontal scroll only.
- `none`: you handle everything. Use sparingly.
- `manipulation`: scroll + pinch-zoom, no double-tap-to-zoom.
## Common mistakes
- **Modal opens, focus stays on trigger.** Tab navigates behind the modal.
- **Modal closes, focus snaps to `<body>`.** User is lost.
- **Tiny close buttons on mobile.** Users mis-tap.
- **Hover styles without the media query.** Flash on phone tap.
- **Suppressing tap highlight with no `:active` replacement.** Touches feel dead.
- **Nested clickables.** Button inside a clickable row fires both. Restructure or use `stopPropagation()`.