Navigation & feedback

Right click a button. If 'Open in new tab' appears, it should be a link.

Links vs. buttons

Put state in the URL. Tab state, sort order, filters become shareable, refreshable, and Back-button friendly:

Most common violation: <button onClick={() => router.push('/x')}>. Use <Link> with the route as its href.

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.

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.

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:

Optimistic updates

When the outcome is predictable (toggles, likes, stars), update the UI immediately and roll back on failure.

Common mistakes