Disabled Buttons Are a UX Smell: What to Do Instead
Disabled submit buttons feel tidy in Figma and hostile in production. Here's why we've stopped shipping them, and the patterns we reach for instead.

You know the pattern. A signup form with six fields, a big primary button that stays greyed out until every input passes validation, and a user staring at the screen wondering what they got wrong. It looks clean in the mockup and it quietly tanks conversion in production.
We've been ripping disabled submit buttons out of client projects for the last two years. Here's the case against them, and the patterns we replace them with.
Why disabled buttons feel right to designers
The logic is seductive: if the form isn't valid, don't let the user submit it. Prevent the error state entirely. Keep the UI in a "safe" state until it's ready.
In a static mockup this reads as considerate. The button is a promise — when it lights up, you're good to go. It also saves the designer from drawing an error screen, because the error screen theoretically can't happen.
The problem is that users don't experience forms as static artefacts. They experience them as a conversation, and a disabled button is the UI equivalent of the other person going silent mid-sentence.
What actually happens in the wild
We've watched enough usability sessions to see the same three failures repeatedly.
Users can't tell what's wrong. The button is disabled, but there's no summary of why. They scan the form, guess, tweak something, and check the button again. It's a guessing game with no feedback loop.
Screen readers get a hostile experience. A button with disabled or aria-disabled="true" is either unfocusable or announced as unavailable. Users on assistive tech can't tab to it, can't activate it, and get no error announcement because — from the DOM's perspective — nothing has gone wrong yet.
Mobile users can't diagnose remotely. On a phone, the button often sits below the fold. Users fill the form, scroll down, see grey, scroll back up, and start hunting. On desktop with a mouse this is annoying. On a bus with one thumb it's a churn event.
There's also a contrast problem. Disabled states typically sit around 30–40% opacity to signal "not available". That routinely fails WCAG contrast requirements for text, and while WCAG technically exempts disabled controls, exempting yourself from accessibility isn't the flex people think it is.
The tell: your button is a validator
Here's the smell in one sentence. If your button's job is to tell the user whether the form is valid, you've made the button do validation's job. Validation should happen at the fields, in plain language, at the moment it matters. The button should just submit.
The patterns we use instead
We reach for one of three approaches depending on the form's complexity and stakes.
1. Always-enabled submit with inline validation
This is our default for anything under about eight fields. The button is always clickable. When the user hits it, we validate everything, focus the first invalid field, and show inline errors underneath each one.
function SignupForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget as HTMLFormElement);
const validation = validate(formData);
if (!validation.ok) {
setErrors(validation.errors);
// Focus first invalid field for keyboard + screen reader users
const firstError = Object.keys(validation.errors)[0];
document.getElementById(firstError)?.focus();
return;
}
submit(formData);
};
return (
<form onSubmit={handleSubmit} noValidate>
<Field name="email" error={errors.email} />
<Field name="password" error={errors.password} />
<button type="submit">Create account</button>
</form>
);
}
Two details matter here. First, noValidate on the form disables the native browser bubbles, which are inconsistent across browsers and hard to style. Second, the errors are announced via aria-live regions on each field, so screen reader users hear what went wrong the moment they submit.
The button is never a source of truth about form state. It's just a submit button.
2. Progressive validation with a live summary
For longer forms — think checkout, KYC, or job applications — we add a summary. The button stays enabled, but somewhere near it we render a small live region:
<div role="status" aria-live="polite" className="text-sm text-slate-600">
{errorCount === 0
? null
: `${errorCount} field${errorCount === 1 ? '' : 's'} need attention`}
</div>
<button type="submit">Continue</button>
The key is aria-live="polite" — it announces changes without interrupting whatever the user is doing. Combined with per-field validation on blur, users get a running sense of where they stand without the button playing gatekeeper.
3. Async work: loading, not disabled
The one case where a button legitimately can't be pressed again is when it's already doing something. Payment submits. Password resets. Anything with a network call.
Even here, we don't disable in the visual sense. We swap the label, show a spinner, and set aria-busy="true". Pointer events are blocked via CSS, but the button remains focusable and the state is announced.
<button
type="submit"
aria-busy={isSubmitting}
className={cn(
'btn-primary',
isSubmitting && 'pointer-events-none opacity-90'
)}
>
{isSubmitting ? (
<><Spinner /> Processing…</>
) : (
'Pay £49'
)}
</button>
Note the opacity is 90%, not 40%. We're signalling activity, not unavailability. And the button retains its identity — same size, same position, same tab index — so nothing reflows and no focus is lost.
When disabled is actually correct
There are cases where a disabled button is the right call. We don't want to pretend otherwise.
- Destructive actions gated on selection. A "Delete 0 items" button on an empty selection genuinely has nothing to do. Disable it, but pair it with a tooltip explaining why on hover and focus.
- Multi-step flows where the next step doesn't exist yet. "Next" on step 3 of a wizard when step 3's content hasn't loaded.
- Feature flags and permissions. If a user genuinely lacks permission to perform an action, disable the button and use
aria-describedbyto point at an explanation.
In all three cases, the button being disabled reflects a real system state, not a validation state. That's the line.
If you must disable, do it accessibly
Use aria-disabled="true" instead of the HTML disabled attribute. The difference matters:
disabledremoves the element from the tab order and blocks all events. Screen readers may skip it entirely.aria-disabled="true"keeps the element focusable and announceable. You block the action in your click handler.
<button
aria-disabled={!canSubmit}
aria-describedby="submit-help"
onClick={(e) => {
if (!canSubmit) {
e.preventDefault();
announceReason();
return;
}
submit();
}}
>
Publish
</button>
<p id="submit-help">Add a title before publishing.</p>
Users can still tab to it, still hear why it's unavailable, and still get feedback if they click. That's the minimum bar.
The measurable impact
We don't have a universal number to give you, and anyone who does is selling something. What we've seen across a handful of e-commerce and SaaS projects: replacing disabled submit buttons with always-enabled + inline validation tends to move form completion rates by a meaningful margin — usually somewhere in the mid single digits, occasionally more on longer forms. Support tickets about "the button doesn't work" disappear entirely.
The accessibility win is harder to measure but easier to justify. A form that works for assistive tech is a form that works for everyone under stress — one-handed, in bright sun, on a bad connection.
Where we'd start
Audit one form. Pick your highest-traffic conversion surface — signup, checkout, contact — and answer three questions.
- Is the primary button ever disabled based on form validity? If yes, that's your target.
- What does a screen reader announce when the form is invalid on submit? Test it with VoiceOver or NVDA. If the answer is "nothing", you have work to do.
- Do your error messages tell the user what to do, or just what's wrong? "Invalid email" is diagnosis; "Add an @ to your email" is a fix.
Rip out the disabled state. Wire up inline errors with focus management. Ship it behind a flag, watch the completion rate for a fortnight, and decide from there. If you want a hand doing this properly across a design system, our team does this kind of work — but honestly, most teams can do it themselves in an afternoon once they stop treating the button as a gatekeeper.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Modals Are a Routing Problem: Rethinking Dialogs in 2026
Most modal bugs aren't CSS bugs. They're routing bugs, focus bugs, and state bugs pretending to be design decisions. Here's how to actually ship dialogs that behave.

Form Field Errors: Why Yours Are Failing Users and How to Fix Them
Inline validation looks helpful on the Figma frame and hostile in production. Here's how to design error states that guide users through a form instead of punishing them for typing.

Toast Notifications Are Broken: A Better Pattern for Async Feedback
Toasts fail screen readers, disappear before users can read them, and stack into unreadable towers. Here's the async feedback pattern we ship instead.
