TL;DR: I propose to add a disabled
attribute to the <a>
element, which would work the same as it already does for the <button>
element. See also http://w3bug.com/28673
Background: Interchangeability
Lemma 1: <button>
and <a>
are largely interchangeable. Visually, <a>
s can be styled into buttons and <button>
s can be styled to look like links. They semantically indicate the opportunity for user interaction (as opposed to e.g. <div>
) and are typically focusable by default. Setting an appropriate click
event handler on a <button>
that sets window.location
approximates an <a>
. Setting an appropriate javascript:
URL on an <a>
, or setting an appropriate click
event handler and either omitting href
or setting a dummy href="#"
approximates a <button>
.
I readily agree that there are many cases where using an <a>
instead of a <button>
, or vice-versa, is suboptimal. Regardless, the fact remains that authors do interchange them for various reasons, some more justified than others. For example:
- "It’s harder (or perhaps impossible) to get my framework/library/template/etc. to emit a
<button>
here instead of an<a>
" - "I need it to get focused when clicked and that’s only possible in all environments when using an
<a>
, not when using a<button>
"
Alternatives to adding a disabled
attribute to <a>
So, we’ve established that <a>
and <button>
are largely interchangeable. Since the designers of HTML saw fit to give <button>
a disabled
attribute that disables it, let us assume that, for any given button-ish widget, it’s reasonable to want some means to disable it. (See e.g. https://github.com/18F/web-design-standards/issues/493#issuecomment-139652953 . This also would have come in handy at Bootstrap.) Not necessarily the same means (this is HTML after all, king of needless inconsistency), but at least one means for each such widget. We now sadly observe that <a>
doesn’t have a disabled
attribute or any other reasonably similar equivalent under some other name. We cry briefly, wipe our tears, pick ourselves up and say: “Okay, no big deal. This is the modern Web™. Surely we can polyfill this?” Well, here are the possibilities I was able to come up with:
pointer-events: none
What if we did something like:
a[disabled] { pointer-events: none; }
a.disabled { pointer-events: none; }
Then the <a>
would no longer receive mouse events, just like a disabled <button>
! Unfortunately:
- This only affects pointing devices, not the keyboard. Users can still
Tab
to the input and emit aclick
on it by pressingEnter
/Spacebar
. And AFAIK, there is nokeyboard-events: none
property to remedy this. - Click events aren’t fully suppressed, but rather will target the element behind the
<a>
instead, which could be problematic in some cases. - The
[disabled]
attribute version will break HTML validation. - This doesn’t communicate semantic disabledness to users of assistive technology, unless the AT software resorts to using heuristics based on the class name, which can easily fail if the author opts to name the class something other than
disabled
(e.g. to comply with some CSS class naming convention or for namespacing purposes). Authors could use the[aria-disabled="true"]
attribute to remedy this, but this requires additional effort and knowledge on the part of authors, which cannot generally be relied upon.
Global event handler
What about:
document.addEventListener('click', function (e) {
if (this.classList.contains('disabled')) {
e.stopPropagation();
e.preventDefault();
}
}, true);
Downsides:
- Same accessibility concerns as
pointer-events: none
- This must run before any other JavaScript; otherwise, other event listeners could get setup before it.
- Third-party code that wants to ascertain or toggle the disabledness of an
<a>
has to “just know” about this convention somehow. - Are folks still familiar with the Capture Phase these days? (I have no solid data here.)
- Is using the Capture Phase performant? (I have no idea.)
Status quo
At this point, I’m out of ideas for drop-in polyfill-style solutions. My guess is that folks are currently coping via:
Explicit check in every click event handler
buttonIshElement.addEventListener('click', function (e) {
if (this.tagName === 'A' && this.classList.contains('disabled')) {
return;
}
// ...actual logic here...
}, true);
// ...repeat for ALL CLICK EVENT HANDLERS that could involve an <a>...
This “solution” leaves much to be desired:
- This adds annoying boilerplate to all my event handlers. A decorator would decrease the number of repeated tokens, but there’d still be some repetition, the extra layer of function call might negatively affect performance, and there’s the danger of human error in forgetting to apply the decorator to every click handler.
- This doesn’t work for event handlers in third-party code.
- Third-party code that wants to ascertain or toggle the disabledness of an
<a>
has to “just know” about this convention somehow. - This doesn’t solve the problem of “I have a normal
<a href='http://...'>
that I want to disable.”. You could remove thehref
and store its value into adata-*
attribute, but it’s a bit klunky and merely a convention. - Writing a generic “toggle the disabledness of a form control” function becomes more painful (likewise for “get the current disabledness of a form control”):
function toggle(elem) {
if (elem.tagName === 'A') {
elem.classList.toggle('disabled');
} else { // input, button
if (elem.hasAttribute('disabled')) {
elem.removeAttribute('disabled');
} else {
elem.setAttribute('disabled', 'disabled');
}
}
}
And I want a generic function for this because I might switch back and forth between using an <a>
or a <button>
to maintain appropriate semantics as my UI evolves. For example: “Yesterday, this was an <a>
that linked to a separate ‘edit’ page. Today, I made it a <button>
that activates a modal ‘edit’ dialog. I don’t wanna rewrite the code that enables/disables the ‘edit’ button/link.”
Proposed solution
The proposed solution is to “simply” add a disabled
attribute to <a>
which would have the same semantics as <button>
's existing disabled
attribute. Since it’s built-in, accessibility will also be built-in, third-party event handlers won’t need to be modified, the HTML validator won’t complain, and it’ll work correctly for keyboard users. And no event handler boilerplate code or magic global event handler will be required.
Well, what do you think? Is there some existing technique I’m unaware of that already solves this problem nicely?