The Problem With Simply Disabling Buttons

There are legitimate reasons to disable a button. The most common is preventing an invalid action — for example, an “Add to cart” button that only becomes valid when the ticket quantity is greater than zero. In that scenario, reaching for the native disabled attribute seems natural:

<button type="submit" disabled="disabled">
  Add to cart
</button>

But the disabled attribute is a boolean attribute that does far more than prevent a click. It also removes the button from the tab order entirely. For keyboard users navigating with Tab and Shift+Tab, the disabled button is simply skipped — focus jumps from the ticket input straight to the next focusable element:

Using the Tab key, it changes the focus from the input to the link, skipping the “Add to cart” button.

The intent behind disabling the button was to block the click. But disabled also blocks focus, hover, and any other form of interaction. What we actually wanted was to stop a specific type of interaction — the click — without cutting off all the others.

Why Focus Matters

There's a meaningful distinction between accessibility and usability. The disabled attribute is technically accessible: it communicates the button’s unavailable state to assistive technologies, and screen reader users can still locate it by navigating through each element one by one. But that process is tedious and confusing.

For a sighted mouse user, a tooltip explaining why the button is disabled is helpful:

Using the mouse, the tooltip on the “Add to cart” button is visible on hover. But the tooltip is missing when using the Tab key.

But that information is invisible to anyone who can't focus the button — keyboard users and touch device users included. The attribute solves the click problem while creating a usability problem elsewhere.

People with cognitive disabilities in particular may struggle to understand why a button that appears on the page can't receive focus at all. From the user's perspective, the button might as well not exist until some hidden condition suddenly makes it available.

Swapping to aria-disabled

Here's where an ARIA attribute can do a better job. The disabled attribute correlates to aria-disabled="true", but with a critical difference: where disabled changes both semantics and behavior, aria-disabled only affects semantics. It tells assistive technologies that the button is in a disabled state without removing it from the tab order or blocking focus.

With aria-disabled="true", keyboard users can still target the button and see the tooltip explaining why it's currently inactive:

Using the Tab key, the “Add to cart” button is focused and it shows the tooltip.

What Actually Changes

The difference between the two attributes becomes clear when you look at their behavior side by side:

Feature / Attributedisabledaria-disabled="true"
Prevent click
Prevent hover
Prevent focus
Default CSS styles
Semantics

There is overlap: both attributes communicate to screen readers that the button is disabled. But their user experiences diverge sharply.

  • disabled skips the button entirely during keyboard navigation, which can confuse people who expect to find it.
  • aria-disabled keeps the button focusable so users know it exists and understand that it isn't enabled yet — the same way someone might perceive it visually.
Tool / Attributedisabledaria-disabled="true"
Mouse or tapPrevents a button click.Requires JS to prevent the click.
TabUnable to focus the button.Able to focus the button.
Screen readerButton is difficult to locate.Button is easily located.

Screen reader behavior differs too. With the native disabled attribute, VoiceOver on macOS completely skips the button in its default navigation mode — an aggravating experience in longer forms where users are looking for a submit button that isn't where they expect it to be. Screen reader users can find the disabled button with individual element navigation, but it's an unnecessarily lengthy detour.

With aria-disabled, the button receives focus normally and properly announces its status — NVDA and JAWS say “button, unavailable” while VoiceOver says “button, dimmed.”

The JavaScript Caveat

Using aria-disabled doesn't automatically block clicks. That's the point: unlike disabled, ARIA attributes never alter default application behavior. If you need to stop the click — for example, to prevent double form submission during loading — that logic must live in JavaScript:

elForm.addEventListener('submit', function (event) {
  event.preventDefault(); /* prevent native form submit */

  const isDisabled = elButtonSubmit.getAttribute('aria-disabled') === 'true';

  if (isDisabled || isSubmitting) {
    // return early to prevent the ticket from being added
    return;
  }

  isSubmitting = true;
  // ... code to add to cart...
  isSubmitting = false;
})

The trade-off is typically worth it. Using the disabled attribute to prevent double submissions temporarily removes keyboard focus from the button while the form is submitting, which leaves keyboard users stranded with no indication of where they are or what happened.

Don't Use pointer-events Either

Another misguided pattern for preventing clicks is the CSS rule pointer-events: none;. Although it does block mouse clicks at the painting layer, it does nothing to stop focus or keyboard activation. This can create unpredictable outcomes — or outright bugs — when a keyboard user activates a “clickable-looking” element that the CSS says should be inert. The mismatch between what's visible and what's actually interactive is worse than either of the HTML attributes.

A Better Alternative Entirely

In many cases, the best choice is not to disable the button at all, even with the more forgiving ARIA version. Instead, let people submit the form whenever they want and use error messages as feedback when the input is invalid.

  • Less cognitive friction: Users never have to wonder whether a button is inactive. They can attempt the action and learn from the outcome.
  • Color contrast: Disabled elements don't have to meet WCAG 1.4.3 color contrast requirements. An always-enabled button sidesteps that concern entirely.

Handling Dynamic Feedback Responsibly

If the button triggers an action that asyncronously updates the page — adding tickets, submitting a form, confirming an order — the result must be announced to screen reader users. Visually perceived changes aren't automatically perceivable through assistive technology.

The solution is a live region. A <span> with an aria-live attribute — hidden with a screen-reader-only class but still present in the DOM — allows assistive technologies to monitor the region and announce content changes as they happen:

<button type="submit" aria-disabled="true">
  Add to cart
  <span aria-live="assertive" class="sr-only js-loadingMsg">
     <!-- Use JavaScript to inject the the loading message -->
  </span>
</button>

<p aria-live="assertive" class="formStatus">
  <!-- Use JavaScript to inject the success message -->
</p>

With aria-live="assertive", a meaningful loading message can be announced after the button is pressed while the form processes. The form feedback element can follow the same pattern.

A critical caveat: aria-live must be present in the DOM from the initial page load, even if empty. Adding it dynamically after the fact can cause assistive technologies to miss the updates entirely.

Weighing the Trade-offs

The disabled attribute isn't categorically wrong — pagination controls and other cases where an action is permanently unavailable still benefit from it. But for transient, state-dependent states like form validation, it damages usability more than it helps.

The honest assessment: disabled is accessible but not particularly usable. It communicates state to assistive technologies, but the navigation experience it creates is frustrating. Swapping to aria-disabled — combined with complementary JavaScript to handle the click prevention, and live regions to announce dynamic changes — preserves the visual design intent while making the experience dramatically more inclusive.

Web accessibility is rarely about finding the single perfect implementation that satisfies every scenario. There will always be trade-offs and compromises. The job is to understand what the available tools actually do and choose accordingly.