Disabled Buttons Are a UX Bug: What to Ship Instead
Greyed-out buttons feel safe to designers and ship-ready to engineers. They're neither. Here's why disabled states quietly tank conversion, and the patterns we use to replace them without breaking validation.

Every product team has shipped this bug and called it a feature: a primary CTA that sits greyed out until the form is "valid." It looks tidy in Figma. It tests fine on the designer's machine. Then real users hit it, can't figure out what's missing, and bounce. Disabled buttons are one of the most expensive UX defaults still alive in 2026.
Why disabled buttons keep shipping
The pattern survives because it solves a problem for the team, not the user. Engineers like it because it short-circuits validation: if the button can't be clicked, you don't have to think about error states. Designers like it because the screen looks calm — no red text, no scary borders. PMs like it because it feels "guided."
The user gets none of that. They get a button that doesn't respond, no explanation of why, and often no visible indication of which field is the problem. On mobile, where the keyboard is covering half the form, this is brutal.
What the research keeps showing
We've run usability sessions on checkout and signup flows for clients in fintech, e-commerce, and SaaS. The pattern is consistent: when a primary action is disabled, a meaningful share of users tap it anyway, get no feedback, and either re-tap (assuming a bug) or abandon. The ones who stay often can't find the offending field, especially on long forms with conditional logic.
NN/g, GOV.UK, and Shopify Polaris have all published guidance against disabling submit buttons for this reason. Polaris is blunt: don't disable a button to prevent submission — let the user submit and tell them what's wrong.
The accessibility tax nobody budgets for
Disabled buttons have specific WCAG implications that most teams handwave.
- Contrast is not required on disabled controls (WCAG 1.4.3 exempts them), which is why designers reach for 30% opacity. But users with low vision often can't tell a disabled button from a loading one, or from a button they've already tapped.
- Screen readers announce "dimmed" or "unavailable" with no reason. NVDA and VoiceOver users get told the action is off, but not what to do next.
disabledremoves the element from the tab order in most browsers, so keyboard users can't focus it to inspect a tooltip — which kills the popular "hover to see why it's disabled" pattern entirely.
If you must communicate "not ready yet," aria-disabled="true" is almost always the better primitive. It keeps the element focusable, announceable, and clickable — so you can show the user why on click.
The pattern we ship instead
The rule we use across client work: the primary action is always clickable. Clicking it either submits or explains.
That sounds obvious until you implement it. Here's the shape of it in React with Tailwind:
function SubmitButton({ form, onSubmit }: Props) {
const [attempted, setAttempted] = useState(false);
const errors = validate(form);
const hasErrors = Object.keys(errors).length > 0;
const handleClick = () => {
if (hasErrors) {
setAttempted(true);
// Move focus to the first invalid field
const firstError = Object.keys(errors)[0];
document.getElementById(firstError)?.focus();
return;
}
onSubmit(form);
};
return (
<button
type="button"
onClick={handleClick}
aria-disabled={hasErrors}
aria-describedby={attempted && hasErrors ? 'form-errors' : undefined}
className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium
px-5 py-3 rounded-lg focus-visible:outline
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-indigo-500"
>
Continue
</button>
);
}
A few things to notice:
- The button has full contrast at all times. No 40% opacity ghost state.
aria-disabledcommunicates the semantic state without removing the button from the keyboard flow.- On click with errors, we move focus to the first invalid field. That single line of code does more for conversion than any visual polish.
- The error summary (
form-errors) is announced viaaria-describedbyonly after a submission attempt — so we're not yelling at the user before they've tried.
When you actually do need a non-actionable state
There are legitimate cases for a button that can't be pressed: an irreversible action mid-flight, a destructive action gated by a confirmation, a feature behind a paywall. In those cases the rule is: show the reason in the same place as the button.
For in-flight actions, swap the label to a loading state with an aria-live region. For paywall gates, replace the button with a link to the upgrade path — don't grey out the action and call it a day. For irreversible operations, use a confirmation dialog, not a permanently inert control.
Validation timing: the other half of the fix
Killing the disabled state without fixing validation timing just trades one bad UX for another. If every click on a half-filled form throws three red error messages, you've made things worse.
The pattern that's held up across our projects:
- Validate on blur, not on every keystroke. Per-keystroke validation punishes users mid-thought.
- Clear errors on input. As soon as the user starts editing an invalid field, the error message goes away. Don't make them re-blur to confirm they fixed it.
- Only validate the whole form on submit attempt. Before that, fields validate themselves independently.
- Summarise on submit. If the user clicks Continue and there are three errors, show an error summary at the top with anchor links to each field. GOV.UK's pattern here is the gold standard and worth copying.
<div
id="form-errors"
role="alert"
aria-live="assertive"
className="border-l-4 border-red-500 bg-red-50 p-4 mb-6"
>
<h2 className="font-semibold text-red-900">
There are {errorCount} issues to fix
</h2>
<ul className="mt-2 space-y-1">
{errors.map(e => (
<li key={e.field}>
<a href={`#${e.field}`} className="text-red-700 underline">
{e.message}
</a>
</li>
))}
</ul>
</div>
Design token implications
If your design system has a button.disabled token, you probably have this pattern baked in across hundreds of components. Migrating away takes a deliberate token strategy.
What we recommend:
- Keep a
button.pendingtoken for genuinely in-flight states (loading spinners, mid-transaction). Distinct visual treatment — usually a spinner, not reduced opacity. - Rename
button.disabledtobutton.gatedif you really need it, and reserve it for paywall or permission cases where the click should still do something useful (open an upgrade modal). - Remove
disabledfrom your default form button variants entirely. Make engineers opt in.
This is a design system change, not a component change. Doing it piecemeal across product teams produces inconsistent behaviour, which is worse than the original bug.
What about destructive actions?
The one place we'll concede a brief disabled state is in confirmation dialogs that require typing a value — "type DELETE to confirm." Even there, we prefer a button that's clickable and explains itself on click, but the typed-confirmation pattern is established enough that users expect the gate.
If you go that route: make sure the disabled styling still meets 3:1 contrast against the background, use aria-disabled over disabled, and put the instruction ("Type DELETE above to enable") next to the button, not in a tooltip.
Where we'd start
If this is the first time you're auditing your product for this, don't try to rip out every disabled button on Monday. Start here:
- Find your top three conversion-critical CTAs — checkout, signup, primary upgrade. Fix those first.
- Instrument click attempts on each. You'll usually find a measurable rate of users tapping buttons that don't respond. That's your baseline.
- Ship the always-clickable pattern with focus-moves-to-first-error behaviour. Measure the delta over two weeks.
- Then tackle the design system tokens, once you've got evidence the team can point to.
If you'd rather have us run the audit with you, that's the kind of work our product design team does on engagements — usually as part of a broader conversion UX review. The disabled button is rarely the only thing we find, but it's almost always on the list.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Skeleton Screens vs Spinners: When Each One Actually Wins
Skeleton screens aren't automatically better than spinners. Here's the decision tree we use on real projects, with code, timing thresholds, and the accessibility traps nobody talks about.

Focus Rings Are Not Optional: A Practical Guide to Visible Focus in 2026
Removing the focus ring is the most common accessibility regression we see in audits. Here's how to keep keyboard users happy without making your designer cry.

Toast Notifications Are Lying to Your Users
Toasts feel modern, but they're quietly failing the people who need them most. Here's how we audit, fix, or replace them — with patterns that hold up under accessibility and conversion scrutiny.
