Contents
Contents
Buttons are the most-clicked element on any interface. They're also the most neglected. The difference between a button that feels alive and one that feels inert is never a single decision — it's five states, each doing a different job.
Think of a button as a state machine: idle → hover → pressed → loading → success or error → idle. When any state is missing or wrong, the interface misses a beat. When all of them work together, the button disappears into the experience.
The starting point. A button at rest communicates one thing: "I can be clicked." Three things must be true:
cursor: pointer — the cursor communicates affordance before hoverThese aren't micro-interactions — they're prerequisites. Get them wrong and everything else is irrelevant.
Hover is a promise. It says: "you're about to do something." The design job is to confirm that promise without over-committing.
The right amount of hover feedback is subtle: a slight background lift, a 1px vertical translation, a soft shadow. The transition should be 100–150ms — fast enough to feel responsive, deliberate enough to read as intentional.
Hover the button. Toggle to compare default vs. designed.
The button without hover feedback gives you nothing to respond to. You're not sure if hovering even registered. The designed version acknowledges you.
This is where physics enters. When a user clicks, they expect the button to compress — to physically respond to pressure. The rules:
scale(0.97) creates compression without distraction.Click the button. Toggle to compare no feedback vs. designed press.
The combination of scale, shadow, and release timing is what makes a button feel like a physical object rather than a pixel state change.
After the click: the async operation. Most buttons fail here. They either do nothing — leaving the user to wonder if their click registered — or they go blank and freeze.
The right approach:
disabled attribute prevents double-submissionvisibility: hidden (not display: none) so it still occupies spaceClick to simulate a 2-second async operation.
The button stays exactly the same size. The label is invisible but still occupies space; the spinner overlays it. No jump on resolve.
The operation resolved. The button needs to close the loop — briefly, then get out of the way.
Success: A green confirmation, a checkmark, ~1.5 seconds, then back to idle. The brevity is intentional — you're confirming, not celebrating.
Error: Red background, failure label, a shake animation, ~1.5 seconds, then idle. The shake matters — it's the tactile signal that something went wrong. The reset matters too: don't strand the user in an error state.
Click either button to see the full arc. Left resolves success, right resolves error.
Both arcs share one rule: always return to idle. An interface that stays locked in a success or error state forces the user to wonder if they can try again.
The complete state machine in one place.
Trigger each state manually. Loading resolves to success after 2s.
Idle → hover → press → loading → success or error → idle. Five states, five seconds of communication. Each one tells the user exactly where they are.