Engineering Journal
Pdf Processor
Pdf Processor

A button that does nothing is worse than an error

2026-07-11

The position

If a UI action can fail (a selection is wrong, a precondition isn't met, the content doesn't support the operation) the interface should say so, immediately, in specific language, every single time. Not sometimes. Not only for the failure modes you thought of first. Every button that can do nothing needs a stated reason when it does nothing, or it isn't a feature. It's a trap with a label on it.

What the industry does

Most editor UIs handle this with disabled states: gray out the button, maybe a tooltip on hover, trust the user to infer the missing precondition from context. Some go further and let the action silently no-op instead. The button stays clickable, clicking does nothing, and the user re-clicks a few times before concluding something's broken.

The reasoning behind disabled-and-quiet is usually aesthetic. A UI covered in toasts feels noisy. A cleanly grayed-out button feels considered. Silence reads as confidence.

Why it fails for this problem class

Disabled states only work when the precondition is cheap to compute continuously and simple enough to communicate by graying out alone. "You have no file open" is that kind of precondition. "The selection you just made spans exactly one block, and this needs at least two" is not: it depends on a selection that changes constantly, and the failure is specific enough that graying-out communicates nothing.

I ran into exactly this building a column-split control for a document editor: select paragraphs, choose a column count, the selection splits into side-by-side columns. There are several distinct ways this can't work: nothing selected, a single element selected, fewer blocks than the requested column count, or the selection sits inside a table or callout box that shouldn't split internally. A disabled button can't distinguish these. It's either permanently enabled, defeating the point, or permanently ambiguous about which of four reasons applies.

And here's the part that actually bugs me: a user who clicks and sees nothing happen doesn't conclude "my selection doesn't meet a precondition." They conclude "this feature is broken," and next time they hit an edge case, they don't reach for the tool at all. Silent failure teaches the wrong lesson for every future interaction, not just this one.

The better approach

Make the action always clickable, always attempt to run, and always explain itself the moment it can't complete:

function applyColumnSplit(cols) {
  const selected = getSelectedBlocks();

if (selected.length === 0) { return showToast('No content found in the selection to split.'); } if (selected.length === 1) { return showToast("Select more than one block. A single element can't be split into columns."); } if (selected.length < cols) { return showToast(Only ${selected.length} blocks selected, can't fill ${cols} columns.); }

// ...perform the split }

Every failure path names the actual constraint, in the vocabulary the user already has ("blocks," "columns," a count), not in implementation terms. The cost is a few extra lines per function. The payoff is that a user who hits an edge case learns the tool's actual boundary instead of losing confidence in the tool.

What you give up

Some visual quiet, and some engineering time. Every action needs its failure modes enumerated up front, which is more thinking than "gray it out and move on." A toast-heavy interface can feel chattier than a minimalist one, and there's real taste in keeping messages short enough to read as helpful rather than naggy.

You also give up the illusion that disabled states are a complete solution. They're right for binary, cheap-to-check preconditions, not a substitute for explaining a specific, content-dependent failure.

When the common pattern is right

Disabled-and-quiet is exactly right when the precondition is simple, stable, and already visible elsewhere in the UI: a save button grayed out because there are no unsaved changes, a delete button disabled because nothing is selected at all. In those cases the reason is either obvious from context or genuinely doesn't need stating.

It stops being right the moment the precondition depends on the specifics of what's currently selected, and there's more than one way to fail. At that point, silence isn't polish. It's just a bug report the user has to write for you, one confused re-click at a time.

Read this post in the full Engineering Journal →