Wichary argues that buttons feel broken not because of visual regressions but because engineers fail to model them as publicly observable state machines. A button's entire job is to communicate whether it can be pressed, whether it is being pressed, and what will happen if pressed — treating it as decoration for an action rather than the action's user-facing contract is the root cause of most UX failures.
Wichary points out that mid-level engineers typically list four states (default, hover, active, disabled), when the real list is closer to a dozen including focus-visible, disabled-because-form-invalid, disabled-because-request-in-flight, loading, and transient success/error states. Most shipped UI bugs — double-fires, clickable spinners, unexplained disabled states — trace back to states that were never explicitly written down.
By submitting the essay to Hacker News where it reached 514 points, nozzlegear amplified the argument that button state modeling is a widely under-appreciated engineering problem. The strong community response signals broad recognition that the enumerated-states framing captures a real gap in how buttons are built.
Wichary highlights that disabled buttons which give no reason for being disabled are a common failure mode — the state is communicated but the causation isn't. A button's contract with the user includes explaining what would need to change for it to become pressable, which most implementations silently omit.
Wichary notes that primary buttons which visually dominate a modal can cause users to click them reflexively without reading the surrounding content. This inverts the button's proper role: instead of communicating an action, it coerces one, which is a violation of the single-job principle.
An essay titled "If you're a button, you have one job" hit 514 points on Hacker News, written by Marcin Wichary on his site unsung.aresluna.org. Wichary — who literally wrote the book on keyboards (*Shift Happens*, ~1,200 pages on the history of the typewriter) — spends the piece dissecting why so many buttons on the modern web are broken, and why the fix isn't visual polish but a clearer mental model of what a button actually *is*.
His argument, stripped to its bones: a button is not a rectangle with a label. A button is a small, publicly observable state machine whose entire job is to communicate, at every instant, whether it can be pressed, whether it is being pressed, and what happens if you press it. That's it. When buttons feel wrong — the confirm dialog that fires twice, the submit that grays out for a beat but still accepts a click, the icon-only control that looks disabled when it isn't — it's almost never a visual regression. It's a state machine that leaked.
The essay walks through examples most engineers will recognize on sight: buttons that show a hover state on touch devices and get stuck; buttons that transition to a loading spinner but remain hit-testable; disabled buttons that offer no explanation for *why* they're disabled; primary buttons that visually dominate a modal so hard that users click them without reading. Each one, Wichary notes, is a case of someone treating the button as decoration for an action rather than as the action's user-facing contract.
Most UI regressions ship not because the CSS is wrong but because nobody wrote down the states. If you asked a mid-level engineer to enumerate every state a submit button can be in, you'd probably hear four: default, hover, active, disabled. The real list is closer to a dozen: default, hover, focus, focus-visible, active/pressed, disabled-because-form-invalid, disabled-because-request-in-flight, loading, success-transient, error-transient, keyboard-focused-while-loading, and the ugly one — the state where the network call succeeded but the response is still in flight to the client. Every one of those needs a distinct visual and a distinct answer to "what does clicking me do right now?" — and most component libraries answer maybe five of them out of the box.
This is where Wichary's framing lands hardest for practitioners. The React/Vue/Svelte ecosystems have spent a decade making components *composable* without making them *stateful in a legible way*. A `
The HN comment thread on Wichary's piece is unusually good — you can see senior engineers converging on the same conclusion from different directions. One thread argues that the button problem is a symptom of a deeper issue: framework primitives model *props*, not *state*, and every team ends up reinventing the debounce-plus-loading-plus-error dance in userland. Another thread points out that browser vendors abandoned the idea of a stateful native button around the time `
There's a career-industry angle here too. Wichary's essay is the kind of writing that a senior engineer can hand to a junior and say "read this before you touch our checkout flow." It reframes UI work as protocol design — every widget is a tiny contract between the user, the DOM, and your backend — and that reframing is exactly what separates engineers who ship reliable UIs from engineers who ship pretty ones that break under load.
The practical move is boring and effective: sit down with your design system and enumerate the states of every interactive primitive. Not the *visual variants* — the *states*. For a button, that means writing out the full finite state machine, including the transitions you've been faking with `setTimeout`. If you use XState or a hand-rolled reducer, model the button explicitly. If you don't, at least make the states enumerable in TypeScript so `exhaustive` switches catch the ones you forgot.
Second, audit your disabled states. A disabled button with no tooltip explaining why it's disabled is worse than no button at all — it's a locked door with the key visible on the other side. Wichary's essay is emphatic on this point, and if you look at your own product with fresh eyes, you'll find at least three of them. Add hover explanations, or replace the disabled state with a visible-but-warning state that tells the user what's missing.
Third, revisit your loading affordance. If you replace the label with a spinner but keep the button clickable, you have a bug. If you disable the button but the label doesn't change, keyboard users won't know they're in a loading state. The minimal correct pattern is: change the accessible name (e.g. "Submit" → "Submitting…"), set `aria-disabled="true"`, ignore further clicks in the handler, and only revert to the resting state after the response is processed *and* rendered.
Wichary's essay won't ship a new library, but it might change how you code review your next PR. The web's next wave of UI improvements probably won't come from a new framework — the last three didn't either. It'll come from teams treating every interactive element as a state machine with a public contract, and finally writing that contract down. If your buttons have one job, make sure they know what it is.
One job doesn't really fit the button thing because a button has to do many things, only one of which is being clickable.Having feedback when clicked, feedback when hovered. A loading state, a disabled state, a mix of everything. That's also something I found very frustrating. Like if you
I want to support the "what about debouncing" argument mentioned elsewhere; the author shouldn't just ignore this.But I also hate the "you had one job" meme and want to argue against its mindless usage. Most of the time, when people do the "you had one job" thing,
The worst part about this is the amount of lag you have when taking a photo. On samsung phones, its quick and seamless, on other android phones the IO is so slow, one picture is taken and before you can tap the shoot button again you have to wait until it saved the previously shot photo. Sometimes t
I used to have a device with a physical button which, when you pressed it, would beep and add 30 seconds to the time. However, sometimes it would beep and not add 30 seconds, and sometimes it would add 30 seconds without beeping, so you always had to squint at the dim display to discover whether it
Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.
How about when users accidentally click too much, or they believe the first click didn’t register?I am still reminded of a keynote where Steve Jobs was demoing how much faster PDF documents would display on the newer macOS. So he had engineers put a button in for him to click that would scroll throu