All articles
Design & UXOctober 4, 2026 6 min read

Disabled Buttons Are a UX Smell: What to Do Instead

Greyed-out buttons feel safe to designers and devs, but they punish users for your form's problems. Here's a pattern-by-pattern replacement guide that keeps buttons alive and errors honest.

Disabled Buttons Are a UX Smell: What to Do Instead

Disabled submit buttons are one of those patterns everyone ships and nobody questions. The form isn't valid, so the button goes grey, and we all move on. The problem: that greyed-out rectangle is usually hiding a design failure — and it's quietly costing you completed forms, support tickets, and WCAG compliance.

This isn't a rant against the disabled attribute. It has legitimate uses. It's a rant against the reflex to disable the primary action every time something upstream isn't perfect.

Why disabled buttons fail users

A disabled button communicates one thing: you can't proceed. It does not communicate why, what to fix, or what you did wrong. The user is left to reverse-engineer your validation rules by staring at a greyed-out pill.

Three concrete failure modes we see in audits:

  1. Silent blocking. User fills what they think is a complete form, taps Submit, nothing happens. On touch, there's no hover tooltip to rescue them. They assume the app is broken.
  2. Invisible focus traps. disabled elements are removed from the tab order in most browsers. A keyboard user tabbing through your form never lands on the button and never gets to trigger the validation message that would explain the problem.
  3. Contrast failures. The default disabled look — grey text on grey background — almost always fails WCAG AA contrast. Designers wave this off because "it's disabled, it doesn't need to be readable." Screen magnifier users and anyone over 50 disagree.

There's also a conversion angle. In e-commerce checkouts we've rebuilt, replacing a disabled "Place order" button with an always-enabled one that surfaces real errors on click tends to recover a measurable chunk of abandoned carts. We won't quote a specific percentage — your mileage varies with form length and audience — but the direction is consistent across the projects we've touched.

The accessibility nuance: disabled vs aria-disabled

These are not the same thing, and the distinction matters.

  • disabled (the HTML attribute) removes the control from the accessibility tree in a hostile way: no focus, no click events, no announcement beyond "dimmed."
  • aria-disabled="true" marks the control as unavailable semantically while keeping it focusable and clickable. You then intercept the click and show the user why nothing happened.

For almost every form submit button, aria-disabled is the right primitive. The ARIA Authoring Practices Guide has been consistent on this for years.

The pattern: always-enabled, honestly-validated

Here's the rule of thumb we use on client work:

If a button looks like it should be clickable, it should be clickable. If the action can't complete, tell the user why after they ask.

In practice this means: keep the button fully enabled visually, let the user click it, and on click either submit or surface the specific blocking error with focus moved to the first invalid field.

// React + Tailwind example
function SubmitButton({ formState, onSubmit }) {
  const isBlocked = !formState.isValid;

  const handleClick = () => {
    if (isBlocked) {
      const firstError = document.querySelector('[data-invalid="true"]');
      firstError?.focus();
      firstError?.scrollIntoView({ block: 'center', behavior: 'smooth' });
      return;
    }
    onSubmit();
  };

  return (
    <button
      type="button"
      onClick={handleClick}
      aria-disabled={isBlocked}
      className="bg-indigo-600 text-white px-4 py-2 rounded-md
                 hover:bg-indigo-700 focus-visible:ring-2
                 focus-visible:ring-indigo-400"
    >
      Place order
    </button>
  );
}

Notice what's missing: no conditional class flipping the button to grey, no disabled attribute, no tooltip explaining the invalid state before the user has even tried.

What "honestly validated" means

On click of a blocked form, you owe the user three things:

  1. Focus moved to the first invalid field (not just a scroll — actual focus, so screen readers announce it).
  2. An inline error next to that field, written in plain language. Not "Invalid input." Try "We need a postcode to calculate shipping."
  3. A summary at the top of the form if there are multiple errors, each one a link to its field. This is a WCAG 3.3.1 pattern and it works beautifully for long forms.

When disabled is the right answer

I promised this wasn't a blanket ban. Keep disabled for:

  • Destructive confirmations with a cooldown. "Delete workspace" that unlocks after you type the workspace name. The disabled state here is a safety feature, not a validation shortcut.
  • Pagination controls at boundaries. The "Previous" button on page 1 of a table. There's nothing to explain — the state is self-evident.
  • Toggles for features behind a plan gate, but only when paired with a visible upgrade affordance. Don't just grey it out; add a lock icon and route the click to the pricing page.
  • Buttons mid-submission. Once a user has clicked Submit and you're waiting on the network, disabling (or better, swapping to a loading state) prevents double-submits.

The common thread: the user understands why the control is unavailable without having to interact with it.

Design token hygiene for button states

A lot of the disabled-button mess comes from design systems that treat "disabled" as a single token applied globally. That's rarely what you want. A disabled primary CTA, a disabled icon button, and a disabled nav item all have different jobs.

At minimum, define states as a matrix in your tokens:

{
  "button": {
    "primary": {
      "bg": { "default": "#4f46e5", "hover": "#4338ca", "unavailable": "#4f46e5" },
      "fg": { "default": "#ffffff", "unavailable": "#ffffff" },
      "opacity": { "default": 1, "unavailable": 0.65 }
    }
  }
}

Note the token name: unavailable, not disabled. That naming nudges designers and engineers to think about the semantic state rather than reaching for the HTML attribute. The opacity drop gives a visual hint without destroying contrast, and the button stays interactive.

Testing contrast properly

Run both the enabled and unavailable states through a contrast checker. If your "unavailable" state drops below 4.5:1 for text, you've built a trap for anyone with low vision. Tools like axe DevTools or the built-in contrast checker in Figma will catch this in seconds — add it to your design review checklist, not just the engineering QA.

A migration path for existing design systems

If your component library already ships <Button disabled> everywhere, you don't need a big-bang rewrite. We've done this incrementally on several design system refactors:

  1. Add an unavailable prop to the Button component that uses aria-disabled and keeps click handlers wired. Keep disabled working for backward compat.
  2. Audit submit buttons first. They're the highest-impact targets. Swap disabled for unavailable and wire up the on-click validation surfacing.
  3. Document the decision tree. In your Storybook or design system docs, add a flowchart: Is this a boundary condition the user already understands? → disabled. Is this a validation state? → unavailable + inline errors.
  4. Lint it. A simple ESLint rule flagging <SubmitButton disabled> in new PRs stops the pattern from regrowing.

If you're working across design and engineering on this kind of system-wide change, our design systems work covers the token-to-component mapping end to end.

The one-paragraph version for your Slack

Disabled buttons silently block users, fail keyboard navigation, and usually fail contrast. Use aria-disabled instead of the disabled attribute on form submits, keep the button clickable, and move focus to the first error on click. Reserve the real disabled attribute for boundary conditions and in-flight submissions where the unavailability is self-evident.

Where we'd start

Open your highest-traffic form today. Count the states its submit button can be in. If one of them is "grey and unexplained," that's your first fix. Replace it with an always-enabled button that, on click, moves focus to the first invalid field with a clear inline message. Ship that one change, watch your completion rate, then roll the pattern out to the rest of the product.

#UX#Accessibility#Design Systems#Forms

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project