Adding a [disabled] attribute to <a>s


#1

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:


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 a click on it by pressing Enter/Spacebar. And AFAIK, there is no keyboard-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 the href and store its value into a data-* 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?


#2

A fundamental principle of the Web architecture is the uniform interface, one implication of which is that if you don’t want to make an assertion about another page, you simply don’t present a link. In user interface design, this is called progressive disclosure.

Links (including <link/>, <a/>, and the Link: HTTP header) and buttons (<button/> and <input/>) are not generally interchangeable, and carry different semantics. A link indicates a directional relationship to another resource from the current one. The act of following a link is always supposed to be ‘safe’ - it won’t change the state of the application, or at least not the persisted data, just the user interface.

Buttons, on the other hand, are used so some action, in conjunction with other form elements, can be activated, thereby modify the state of the underlying data and/or server.

This is not just a user interface issue, but an accessibility issue. Automated robots, or people using a hypermedia API, will treat buttons and links fundamentally differently.

It’s harder (or perhaps impossible) to get my framework/library/template/etc. to emit a <button> here instead of an <a>

Which seems more impossible, getting this change into user agents, and breaking the semantics of links; or just fixing the framework/library/template in question?

"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>"

Why not fix this directly? Either case will involve modifying user agents. Fixing this directly will mean fixing only the problematic ones; changing the semantics of links means recognizing the feature in all (hypermedia) user agents.

Also consider that, in some cases, this is a feature. Since links and buttons are different, accessibility tools will want to activate them differently, too.

Here is the main issue:

What does it mean when you have link in a document, it’s in the DOM tree, it has a href to another resource, but it’s “disabled”? It doesn’t serve any purpose to say “this is a link to another page (thus creating a formal relationship between the two resources), but we won’t let you follow it.”

Overall, applying this technique, I can’t say I’ve ever run into this issue. If you absolutely need a placeholder for a link for UI purposes, render a span. Programmatically applying this change to the DOM, worst case, means:

  1. var span = document.createElement('span')
  2. Copy the href (if necessary, e.g. into a data-href attribute)
  3. Move the children into span
  4. Append span right before the obsolete link
  5. Delete the old link

jQuery can probably do it in two lines:

var href = $('#link').attr('href');
$('#link').replaceWith('<span>' + $('#target').html() +'</span>').data('href', href)

#3

I’ve hit this boundary as well and did some tests. Here’s what ally.js element/disabled will do to render any element disabled:

  • adding tabindex="-1" attribute to remove element from document’s focus navigation sequence
  • adding the focusable="false" attribute on SVGElement
  • removing the controls attribute from <audio> and <video> elements
  • overwriting element.focus() to prevent focusing the element by script
  • adding the CSS property pointer-events: none; to prevent any interaction from mouse and touch
  • adding aria-disabled="true" to inform the AccessibilityTree of the element’s state

You’re proposing disabled only for <a>. I believe we need this for all interactive content (which basically is everything that can receive focus). I believe this is more or less what the inert attribute would’ve done, if it hadn’t been dropped from HTML5.


#4

I agree that it would be sensible and useful to extend [disabled] to all interactive elements. But my guess is that this more modest proposal would be more likely to succeed over a shorter timescale. More general proposal = More time needed to discuss+spec+implement.


#5

if you don’t want to make an assertion about another page, you simply don’t present a link. In user interface design, this is called progressive disclosure.

Yes, and I’m pretty sure there’s another UI design principle that favors disabling UI elements when they’re not applicable rather than removing them entirely, so as to aid spatial memory and to show the universe of options that the application generally supports. Some apps go a step further and even show a tooltip explaining why the element is currently disabled and what action would be necessary to enable it.

A link indicates a directional relationship to another resource from the current one. The act of following a link is always supposed to be ‘safe’ - it won’t change the state of the application, or at least not the persisted data, just the user interface.

Resources that use GET where they should use POST are indeed bad, but my argument is in no way predicated on accepting that bad practice.

Buttons, on the other hand, are used so some action, in conjunction with other form elements, can be activated, thereby modify the state of the underlying data and/or server.

“In the wild”, on the real web, buttons and links don’t fall into such neat-and-tidy categories. For example, if I have a <button> that summons a modal dialog to edit the details of an entity, no server state is changed. Folks might quibble about whether or not the modal deserves its own URL or not, in which case an <a> could be more appropriate, but whether the control that opens the dialog is a <button> or an <a> doesn’t make a huge difference in practice.

Which seems more impossible, getting this change into user agents, and breaking the semantics of links; or just fixing the framework/library/template in question?

I’m approaching this with my Bootstrap-contributor hat on. I’m two levels of indirection away from being able to fix such libraries (I work on Bootstrap; Bootstrap is used on someone’s webpage; and that webpage was created using library X) and there are several such libraries.
So, equally impossible from where I stand.

"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>"

Why not fix this directly?

I would love for this to be fixed directly. But this is merely one of what I’m sure are several arcane examples of ways in which browsers needlessly (and from a developer perspective, annoyingly) treat <a> and <button> differently. I expect that eradicating these differences would require a timescale equal to or greater than that of adding <a disabled>. And we’d still be left with an unpleasant developer experience when an <a> was indeed semantically appropriate but disable-ability was also required.

Here is the main issue:

What does it mean when you have link in a document, it’s in the DOM tree, it has a href to another resource, but it’s “disabled”? It doesn’t serve any purpose to say “this is a link to another page (thus creating a formal relationship between the two resources), but we won’t let you follow it.”

It means that the link is not currently applicable, just like how the action that a disabled <button> would perform is not applicable while the button is disabled. You might as well claim that disabled <button>s serve no purpose; clearly they do.

Overall, applying this technique, I can’t say I’ve ever run into this issue. If you absolutely need a placeholder for a link for UI purposes, render a span. Programmatically applying this change to the DOM, worst case, means:

  1. var span = document.createElement('span')
  2. Copy the href (if necessary, e.g. into a data-href attribute)
  3. Move the children into span
  4. Append span right before the obsolete link
  5. Delete the old link

A. This thrashes a DOM a bit.
B. Can’t do (5) because the link might have event handlers attached to it and this would irrevocably destroy those attachments.
C. If the link has other attributes that need to be preserved (e.g. more data-*="..." attrs), the code gets longer.
D. What if I don’t have the luxury of using jQuery? This code becomes much uglier, especially compared to “just toggle the disabled attribute”.
E. This is semantically worse in that it does not express that there is the possibility of the link becoming applicable again if circumstances change. Whereas the <button disabled> equivalent would express this.

The annoyingness of disabling/replacing an <a> is one of the better reasons, amongst a set of mostly bad reasons, to justify authors’ use of the indeed-quite-horrible <button onclick="window.location = '...';"> construction rather than a simple <a>.


#6

Unfortunately for me this seems to be begging the question… What does it mean to be “not currently applicable”?

If this is strictly a user-interface attribute that can be used anywhere, I would suggest it should not be used on links – the mere existence of a link makes an assertion, whether you can interact with it or not. The link element is the quintessential example of this phenomenon.

Same thing with input elements. A text field can be editable, disabled, or even hidden, in the form of type="hidden"

The “disabled” state suggests “this is what will submit the form, but you can’t use it right now because of the current state of the application.” So it works well as a placeholder. It also retains its semantic meaning with respect to the form.

I don’t think this makes sense to do with links. The equivalent of a hidden form element for links is the link element (or Link HTTP-header). The equivalent of this ‘disabled’ link is a placeholder text of some sort.

The only semantics I could think of giving a “disabled” link is to say that “yes, this is a link, but there’s nothing to dereference at the other side”, or nothing to dereference yet. Suppose you want to make a reference to a non-URL URI, like a UUID or ISBN. It doesn’t make sense to dereference these kinds of links. These kinds of links can also be minted even with the http: scheme, too, even though they’re not supposed to be used as URLs, only as URIs.

With these semantics, the attribute would not be HTML specific, but instead would need to be a general link attribute so it can be used in all hypermedia formats that support Web links, in the same fashion as type, title, hreflang, etc.

However, it doesn’t sound like this is quite what you’re doing.

It seems like a better proposal here is to add a DOM method to convert element types. What if we want to convert an input type="text" to a textarea? A solution to convert between elements would fix this, too.