Navigation & feedback
Right click a button. If 'Open in new tab' appears, it should be a link.
## Links vs. buttons
- **`<a href="...">`** for navigation. Supports Cmd+click, middle-click, copy link, bookmarks.
- **`<button>`** for actions on the current page: modals, forms, toggles.
Put state in the URL. Tab state, sort order, filters become shareable, refreshable, and Back-button friendly:
```tsx
<Link href={`/dashboard?tab=${tab}&sort=${sort}`}>Dashboard</Link>
// vs
<button onClick={() => setTab(t)}>Dashboard</button>
```
Most common violation: `<button onClick={() => router.push('/x')}>`. Use `<Link href="/x">`.
## Loading delay: 150-300ms before the spinner
### Show-delay (~150-300ms)
Show nothing for the first 150-300ms. If the operation finishes, it feels instant.
```tsx
const [showSpinner, setShowSpinner] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setShowSpinner(true), 200);
return () => clearTimeout(timer);
}, []);
```
### Minimum-duration (~300-500ms)
Once the spinner appears, keep it for at least 300ms.
## Toasts and live regions
1. **An `aria-live="polite"` container** so screen readers announce them.
2. **A keyboard-reachable dismiss action.**
3. **Persistence near the field** for errors.
The live region must exist in the DOM from page load.
```html
<div role="status" aria-live="polite" aria-atomic="true">
{/* Inject toast text here when events fire */}
</div>
```
Use `aria-live="polite"` for almost everything. Reserve `assertive` for emergencies: session expiry, payment failure.
## Destructive confirmation
1. **Name the action.** "Delete project", not "Are you sure?"
2. **Name the consequences.** "This will permanently delete all 47 invoices."
3. **Make the destructive button visually distinct.** Red, secondary position.
For irreversible actions, require typing the resource name:
```
Type "production-database" to confirm deletion.
```
## Optimistic updates
When the outcome is predictable (toggles, likes, stars), update the UI immediately and roll back on failure.
## Common mistakes
- **`<button onClick={() => router.push('/x')}>`.** Loses link semantics. Use `<Link>`.
- **`<a href="#">` with a click handler.** Looks like a link, isn't one. Use `<button>`.
- **Spinner on every render.** Fast navigation causes loading flashes.
- **No loading state for slow operations.** User clicks Submit twice. Two submissions.
- **"OK" and "Cancel" on destructive dialogs.** Name the action: "Delete project" / "Keep project."
- **Error toast that auto-dismisses.** Persist errors near the field.