Every control shows its state

Hover, press, focus, disabled and loading, all without changing size.

From refactoring-ui, after Refactoring UI by Adam Wathan and Steve Schoger.

Five states, one button

Hover, press and Tab to Send, then click it fast.

Browser defaultsBad
INV-1042Draft

Acme Corp · $4,280.00

Edited 2 min ago
  • hover
  • active
  • focus-visible
  • loading

outline: none
Every state visibleGood
INV-1042Draft

Acme Corp · $4,280.00

Edited 2 min ago

Unlocks after sending.

  • hover
  • active
  • focus-visible
  • loading

:active scale(.97):focus-visible outlinearia-busy
.btn { display: inline-grid; }
.btn > * { grid-area: 1 / 1; }
.btn:active { transform: scale(.97); }
.btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn[aria-busy="true"] .label { opacity: 0; }

Keyboard-only walk

Click Email, then press Tab and Shift+Tab.

outline: noneBad

nothing
:focus-visible ringGood

nothing
:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

Delay the spinner

Save both at Fast, then at Slow.

Instant spinnerBad

delay: 0 ms
Delayed spinnerGood

delay: 400 ms
const t = setTimeout(() => (button.dataset.spinner = ""), 400);
try { await work(); } finally { clearTimeout(t); delete button.dataset.spinner; }

Why it matters, pros and cons

Why it matters

Every input needs an answer: seen, got it, working, or not now. Without one, people double-click or lose their place.

Pros

  • Press feedback makes the UI feel fast.
  • A busy state blocks double submits.
  • Fixed-size loading causes no layout shift.

Cons

  • Five states per control is real work.
  • Hover does not exist on touch screens.
  • Disabled buttons need a reason shown nearby.