The topic of disabling links popped up at my work the other day. Somehow, a “disabled” anchor style was added to our typography styles last year when I wasn’t looking. There is a problem though: there is no real way to disable an <a>
link (with a valid href
attribute) in HTML. Not to mention, why would you even want to? Links are the basis of the web.
At a certain point, it looked like my co-workers were not going to accept this fact, so I started thinking of how this could be accomplished. Knowing that it would take a lot, I wanted to prove that it was not worth the effort and code to support such an unconventional interaction, but I feared that by showing it could be done they would ignore all my warnings and just use my example as proof that it was OK. This hasn’t quite shaken out for me yet, but I figured we could go through my research.
First, things first:
Just don’t do it.
A disabled link is not a link, it’s just text. You need to rethink your design if it calls for disabling a link.
Bootstrap has examples of applying the .disabled
class to anchor tags, and I hate them for it. At least they mention that the class only provides a disabled style, but this is misleading. You need to do more than just make a link look disabled if you really want to disable it.
Surefire way: remove the href
If you have decided that you are going to ignore my warning and proceed with disabling a link, then removing the href
attribute is the best way I know how.
Straight from the official Hyperlink spec:
The
href
attribute ona
andarea
elements is not required; when those elements do not havehref
attributes they do not create hyperlinks.
An easier to understand definition from MDN:
This attribute may be omitted (as of HTML5) to create a placeholder link. A placeholder link resembles a traditional hyperlink, but does not lead anywhere.
Here is basic JavaScript code to set and remove the href
attribute:
/*
* Use your preferred method of targeting a link
*
* document.getElementById('MyLink');
* document.querySelector('.link-class');
* document.querySelector('[href="https://unfetteredthoughts.net"]');
*/
// "Disable" link by removing the href property
link.href = '';
// Enable link by setting the href property
link.href = 'https://unfetteredthoughts.net';
Styling this via CSS is also pretty straightforward:
a {
/* Disabled link styles */
}
a:link, a:visited { /* or a[href] */
/* Enabled link styles */
}
That’s all you need to do!
That’s not enough, I want something more complex so that I can look smarter!
If you just absolutely have to over-engineer some extreme solution, here are some things to consider. Hopefully, you will take heed and recognize that what I am about to show you is not worth the effort.
First, we need to style our link so that it looks disabled.
.isDisabled {
color: currentColor;
cursor: not-allowed;
opacity: 0.5;
text-decoration: none;
}
<a class="isDisabled" href="https://unfetteredthoughts.net">Disabled Link</a>
Setting color
to currentColor
should reset the font color back to your normal, non-link text color. I am also setting the mouse cursor to not-allowed
to display a nice indicator on hover that the normal action is not allowed. Already, we have left out non-mouse users that can’t hover, mainly touch and keyboard, so they won’t get this indication. Next the opacity is cut to half. According to WCAG, disabled elements do not need to meet color contrast guidelines. I think this is very risky since it’s basically plain text at this point, and dropping the opacity in half would make it very hard to read for users with low-vision, another reason I hate this. Lastly, the text decoration underline is removed as this is usually the best indicator something is a link. Now this looks like a disabled link!
But it’s not really disabled! A user can still click/tap on this link. I hear you screaming about pointer-events
.
.isDisabled {
...
pointer-events: none;
}
Ok, we are done! Disabled link accomplished! Except, it’s only really disabled for mouse users clicking and touch users tapping. What about browsers that don’t support pointer-events
? According to caniuse, this is not supported for Opera Mini and IE<11. IE11 and Edge actually don't support pointer-events
unless display
is set to block
or inline-block
. Also, setting pointer-events
to none
overwrites our nice not-allowed
cursor, so now mouse users will not get that additional visual indication that the link is disabled. This is already starting to fall apart. Now we have to change our markup and CSS…
.isDisabled {
cursor: not-allowed;
opacity: 0.5;
}
.isDisabled > a {
color: currentColor;
display: inline-block; /* For IE11/ MS Edge bug */
pointer-events: none;
text-decoration: none;
}
<span class="isDisabled"><a href="https://unfetteredthoughts.net">Disabled Link</a></span>
Wrapping the link in a <
span
>
and adding the isDisabled
class gives us half of our disabled visual style. A nice side-affect here is that the disabled class is now generic and can be used on other elements, like buttons and form elements. The actual anchor tag now has the pointer-events
and text-decoration
set to none
.
What about keyboard users? Keyboard users will use the ENTER
key to activate links. pointer-events
are only for pointers, there is no keyboard-events. We also need to prevent activation for older browsers that don’t support pointer-events
. Now we have to introduce some JavaScript.
Bring in the JavaScript
// After using preferred method to target link
link.addEventListener('click', function (event) {
if (this.parentElement.classList.contains('isDisabled')) {
event.preventDefault();
}
});
Now our link looks disabled and does not respond to activation via clicks, taps, and the ENTER
key. But we are still not done! Screen reader users have no way of knowing that this link is disabled. We need to describe this link as being disabled. The disabled
attribute is not valid on links, but we can use aria-disabled="true"
.
<span class="isDisabled"><a href="https://unfetteredthoughts.net" aria-disabled="true">Disabled Link</a></span>
Now I am going to take this opportunity to style the link based on the aria-disabled
attribute. I like using ARIA attributes as hooks for CSS because having improperly styled elements is an indicator that important accessibility is missing.
.isDisabled {
cursor: not-allowed;
opacity: 0.5;
}
a[aria-disabled="true"] {
color: currentColor;
display: inline-block; /* For IE11/ MS Edge bug */
pointer-events: none;
text-decoration: none;
}
Now our links look disabled, act disabled, and are described as disabled.
Unfortunately, even though the link is described as disabled, some screen readers (JAWS) will still announce this as clickable. It does that for any element that has a click listener. This is because of developer tendency to make non-interactive elements like div
and span
as pseudo-interactive elements with a simple listener. Nothing we can do about that here. Everything we have done to remove any indication that this is a link is foiled by the assistive technology we were trying to fool, ironically because we have tried to fool it before.
But what if we moved the listener to the body?
document.body.addEventListener('click', function (event) {
// filter out clicks on any other elements
if (event.target.nodeName == 'A' && event.target.getAttribute('aria-disabled') == 'true') {
event.preventDefault();
}
});
Are we done? Well, not really. At some point we will need to enable these links so we need to add additional code that will toggle this state/behavior.
function disableLink(link) {
// 1. Add isDisabled class to parent span
link.parentElement.classList.add('isDisabled');
// 2. Store href so we can add it later
link.setAttribute('data-href', link.href);
// 3. Remove href
link.href = '';
// 4. Set aria-disabled to 'true'
link.setAttribute('aria-disabled', 'true');
}
function enableLink(link) {
// 1. Remove 'isDisabled' class from parent span
link.parentElement.classList.remove('isDisabled');
// 2. Set href
link.href = link.getAttribute('data-href');
// 3. Remove 'aria-disabled', better than setting to false
link.removeAttribute('aria-disabled');
}
That’s it. We now have a disabled link that is visually, functionally, and semantically disabled for all users. It only took 10 lines of CSS, 15 lines of JavaScript (including 1 listener on the body), and 2 HTML elements.
Seriously folks, just don’t do it.
Yeah, it seems to me that if there is a need for this, there probably is a better approach to what is trying to be accomplished.
Agreed. Not sure why anyone would want to disable links on the internet. Thats like closing all the rides at Disneyland.
Ahhmm. Of course it makes sense. Links might temporarily be unavailable or unavailable under certain conditions or time frames. Insisting it must be a link send odd. A disabled button will do the trick without all the mess. Just style it as an inline link! *Drops the mic
(pics up mic) Samuel, using a button would be an incorrect use of semantics. Links are meant for navigation/ context change, either to a completely different URL or another in-page location, while buttons are meant to perform dynamic changes on the page. Both have their proper usages and they should not be mixed, or else you will confuse users that rely on those semantics to understand how to interact with and the result of that interaction. You should check out this Links vs Buttons presentation by Marcy Sutton for more information.
I get it — don’t do it, but why not just have the JS handler, drop the extra markup and have the .disabled or [aria-disabled] CSS selector without the pointer-events property? Seems effective enough.
Hi Stephen! I wasn’t trying to make it easy, but definitely wanted to at least be thorough and be as inclusive as possible in providing the same experience for everyone. I struggle with providing a solution, as mentioned, but I am hoping everyone will recognize how silly it is and just not do it.
Really, this is just an attempt to get people to think and maybe learn a few things about HTML, CSS, and ARIA that they didn’t know before.
Let’s be honest, doing this is just nuts. I’ve been making websites since there was a web, including corporate clients. I have never had a need for this and cannot fathom an actual real-life use case for doing such a thing. Kids today…
Agreed. Sadly, it comes up quite a bit. Especially is OSS libraries/ frameworks. Hopefully the kids today will take heed and not do it. It’s not just a developer issue, is a design issue as well.
I have a use case.
In the course platform that my team is using, the platform shows lessons that shouldn’t have access to until completing a previous lesson.
When you click on those lessons, it takes you to a page that says, “You don’t have access.”
It’s not good design to essentially be sent to an error page. So, I overrode their code to disable the link.
Hi Rocky, I really think in your case that removing the href and creating a placeholder link is actually a really good way to solve your problem. Ultimately, those don’t need to be links, they really are just static text. For some users, they are expecting to use the link in a certain and your extra JS is preventing them from their expected interaction, without explaining why.
Have you thought about creating a react component with some fancy ES7 syntax sprinkled in there, so you absolutely need webpack+babel to disable those pesky links?
/s
Dominic, that’s funny. I literally LOL.
If you remove the HREF, as you describe early on in the article, then the anchor is no longer keyboard focusable, and therefore screen readers would not “see” it…
…or am I wrong?
Hi Basher, yes removing the HREF would essentially create a placeholder link. Since it is not actionable, it should not be focusable. Screen reader would still have access to it, it is not hidden. It’s not actionable anymore. Hence, a “disabled” link.
iOS VoiceOver skips invalid links entirely (or at least it used to), so using
href="#"
or something similar would be a better solution than removing thehref
entirely.James, I think thats more of a user agent defect that I would not rely on, it’s a bit of a hack. Ultimately, you are only targeting a very small subset of users and leaving out a lot of other users.
I agree with not doing it. But what about setting
tabindex="-1"
instead of using a click handler topreventDefault()
. This way the link is not clickable or keyboard focusable, and then the screen reader should not announce it.Hey Corey, adding
tabindex="-1"
would not remove the default behavior or semantics, so it would still be actionable (clickable). It may not be focusable by tabbing, but it would still be discoverable and actionable to assistive technologies with special keyboard shortcuts or in list of elements by type.Right, I wasn’t saying it would remove default behavior or semantics, I was simply replying to this section:
So instead of using JavaScript to
preventDefault()
, you could use tabindex=”-1″. Assistive technologies shouldn’t read anything aloud or present that element as interactive.Hi Corey,
tabindex="-1"
will do some other things with assistive technologies that I am not sure you would expect, and would probably cause other issues. As long as the element still has link semantics on it, it will be interactive and discoverable by screen reader users. Addingtabindex="-1"
would not change that.Just a note regarding styling “enabled” links in your example
I would advise to remove the
a
, ina:link, a:visited
.It bumps up the specificity to 11, whereas
:link, :visited
would have a specificity of 10.Having a specificity of 11 would mean that it wouldn’t be overwritten by a classic
.button
class, which has a specificity of 10.Also,
:link, :visited
should be preferred to[href]
, as[href]
would validate despite being empty.Hi Johan, thanks for the comment. Ultimately, you would need to use whichever selectors works with your implementation, this is just to get the point across.
Don’t do it. Got it.
So, let’s say there’s a hypothetical legacy web application that tries to employ this tactic before someone who reads blog posts like this and knows better started working on it to prevent the atrocity…
Recommendations for suitable alternatives that look at least roughly similar? Scenario being that business rules of the current page restrict the links until conditions are met, but should still display them. Minimal JS and HTML dealing with the “link” itself, obviously, but at least a line of JS to toggle the state (say, toggling a CSS class)… Same accessibility considerations.
Thoughts?
Use the placeholder
<a>
element without anhref
if you have an element that will sometimes be a link but isn’t currently.You could use a
data-*
attribute to keep track of the URL that will be used when the link is active. One line of code for each direction of change:Use styles as necessary to distinguish between
:link
and:not(:link)
elements.If your making something where you want to require a pause after load so the viewer is forced to read before clicking. Idk I can see several uses for it when dealing with some more complex dashboard designs or intro’s
I’m curious… what’s the actual use case for this? I can’t think of anytime when I’d want something to look like a link but not actually be a link.
It’s not that you want something to look like a link…
it’s more for something that was a link, but you need to not make it a link with code.
Two use cases that were brought up in the comments were:
1) (My comment) When someone else’s program created a link that – depending on the status of the user, would either go to an error page or the correct page. I disabled the link until the status was correct.
2) For intros/tutorials, where you disable a link until the user ‘completes’ something.
Hello Gérard. To add to Corey’s comment, in addition to tabindex=”-1″, I would add the aria-hidden=”false” attribute. That will hide even regular good old links. Then, the wrapping SPAN tag can have an aria-hidden attribute that reads whatever you want it to sound like. For instance “Blah blah blah. Disabled link”.
It is so much fun doing useless things just to prove that we can LOL.
… But folks, don’t try this at the office. Try it at home instead. LOL.
The only possible reason I can see for a disabled link style is to show the link is now broken… In that case you’d still want it to look like a link (just with a slight change to indicate it’s broken) and doesn’t really matter if the user can click it.
Why not use
touch-action: none
for mobile users? Regardless, you are right this is weird.You haven’t even touched on the (archaic)
<a name="..." >
tag, which used to be the recommended use for link targets. (The part that comes after#
in an URL.) These would now be marked as disabled links, even though they’re not at all links.Even though it’s more common to used
id
s for link targets nowadays, I know of many CMSes that still insert<a name>
.Here is a potential use case…
I run a website that has links to all of my students portfolio websites. For various reasons, sometimes their websites go down (domains expire, hosting expires, they accidentally delete their site, etc).
We encourage industry and potential employers to go to the site. I don’t want these people to visit a broken website. In this case I’d like to leave the student’s name on the list as one of the graduating class, but unlink their site until it’s fixed. I have a WordPress plugin that scans for broken links once every 24 hours, and can take action like applying a class to the broken links.
Alan Nugent wrote in mentioning you could add a tabindex=”-1″ to the links as part of the strategy, since the link is no longer functional, it may not be useful to be able to be in focus at all.