We had a question come up the other day on ShopTalk about regular ol’ anchor links on iOS, and some weird situation where you couldn’t just tap them once to go to the link, it required tapping the link twice. I’ve experienced this myself and have been pretty confounded.
My first thought was there was some kind of bizarre unexpected JavaScript happening. Perhaps a click handler with preventDefault()
on that first click and then getting removed. I couldn’t find anything like that happening though. I’m sure I tried a few more things, but ultimately gave up and used FastClick to make sure those link clicks worked. FastClick wasn’t meant to solve this problem, it’s more to solve the 300ms delay for tapping links that some mobile browsers impart so they can wait to see if you’re doing a double tap (note: not as much of a problem as it once was). Not quite the right tool for the job, but it worked.
The thing is, it wasn’t a JavaScript problem at all, it was a CSS issue.
Nicholas C. Zakas documented this ages ago:
This is where the people at Apple might have been a bit too smart. They realized that there was a lot of functionality on the web relying on hover states and so they figured out a way to deal with that in Safari for iOS. When you touch an item on a webpage, it first triggers a hover state and then triggers the “click”. The end result is that you end up seeing styles applied using :hover for a second before the click interaction happens. It’s a little startling, but it does work. So Safari on iOS very much cares about your styles that have :hover in the selector. They aren’t simply ignored.
(Thanks to Pete Droll for pointing this out to me.)
Here’s two lines of CSS that will cause the problem
a::after {
display: none;
content: "pseudo block!";
}
a:hover::after {
display: inline;
}
On a browser with a cursor pointer, you’ll see the pseudo element reveal itself on :hover

But clicking that link will not prevent the link from being visited. On iOS though, tapping the link will just reveal the pseudo element. It requires a second tap to actually go to the link.

Android doesn’t seem to do this. It will quickly reveal the pseudo element, but also just go to the link as normal.

This business of adding a pseudo element to a link though… not very common right? I’d say that’s true, it’s not super common. Which I suppose is why this isn’t as well known as we might think and only bites people once in a while.
I’ve seen people use pseudo elements to do aesthetic things though, like adding a more-controlled underline to text. So if that happened on :hover only, blammo, trouble has arrived. It does appear to be on hover only by the way, not focus or active.
Update December 2019: In testing the demo here in Safari/iOS 13, it appears like the pseudo-element shows up briefly then the link is followed, not requiring a double-tap. I haven’t yet done any deeper testing across mobile platforms.
It’s not just pseudo elements
This is true for any child element. Remember the thinking behind this was situations in which additional content is shown only on hover. It’s probably more common that an actual element is in use.
For example:
...
<li>
<a href="#0">I'm a thing in a list</a>
<span class="controls">
<button>Do Something</button>
</span>
</li>
...
li .controls {
visibility: hidden;
}
li:hover .controls {
visibility: visible;
}
Hovering over the list item reveals some controls. Because a parent element how has a hover state that reveals content, it will block the anchor link from working with a single tap.
It doesn’t have to be a parent, it could be the link itself:
<a href="http://link.com">
Link
<span>Extra Stuff</span>
</a>
a span {
display: none;
}
a:hover span {
display: inline-block;
}
Media query help
It’s tempting to be like… OK I’ll just apply these hovers on “desktop” sites and pick a media query like…
@media (min-width: 500px) {
a span {
display: none;
}
a:hover span {
display: inline-block;
}
}
…which works in simple tests, but browser window width isn’t the perfect way to test if you have a cursor and “normal” hovers available.
Fortunately, there is a media query for pointers that could be useful to us:
@media (pointer: fine) {
a span {
display: none;
}
a:hover span {
display: inline-block;
}
}
Cool.
There is also a spec for a straight-up hover media query:
@media (hover) {
}
Both of these styles of media queries worked in Chrome and Safari for me, but not Firefox (support level chart), which makes it a litttttle to risky to use, perhaps. Even JavaScript methods to detect touch are questionable, I hear, and will always be weirdly wrong on devices that support both.
UPDATE March 2019: Firefox 64 dropped in December 2018, which included support for hover media queries, making the support board a ton better for this. That’s probably the best solution here.
It’s probably best to just not rely on hover to reveal anything
The tech to work around it isn’t quite there yet.
If anything, design your site such that clicks or taps are required to reveal other things but make that as obvious as you can and don’t trap normal unwilling links inside of those elements.
Trent Walton was probably right six years ago:
Ultimately, I think seeing hover states fade away will make the web a better place. There never has been any substitute for concise content, clear interaction, and simple design. If we focus on core elements that make browsing the web great, our sites will function properly no matter how people use them.
Demo
Here’s one you can play with.
That’s the best advice. If you want to provide nice hover effects for users with a mouse I like to hide those things behind a
has-touch
flag using something like Modernizr.Unfortunately, though, that technique breaks down on most modern Windows devices, which mix touch, mouse, and often pen input. Even Android phones and tablets, and iPads, support mouse input and quite often stylus input. And people often mix input modes — when I’m using a stylus, I quite often scroll with my finger. And I may swap between mouse and touch when I pick my device up to go into a meeting.
I really don’t think that there is a “safe” solution to this — so I agree with you, and the article, that the best approach is to just avoid using hover-states to reveal anything.
As said, unfortunately the media queries are not widely supported yet.
What I am currently using is a tiny javascript where I check for touch support and mouse movement, which works fine for me. It is still not perfect, but better than just watching for touch support or just relying on media queries which are not widely supported at the moment.
This code block add the classes ‘touch’ and ‘mouse’ to html-element, if touch or a mouse is detected.
Imho, best way to handle this is to build the website without relying on “hover” and “enhance” for devices without touch capabilities :)
For example, Modernizr could be used to detect devices without touch capabilities, which mean :
– default case => no hover,
– desktop with touch capabilities => no hover,
– and for all other cases => hover.
Here is an example on http://albinism.ohchr.org/
– default case => no hover (no fade to color),
– desktop with touch capabilities => no hover (no fade to color),
– and for all other cases => hover (ok)
Anyway, it is explained in Apple doc : https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html#//apple_ref/doc/uid/TP40006511-SW7
I went through this particular issue some days ago. I had an image and some text wrapped in an anchor, when the user hover the image the text was shown over the image otherwise it was hidden with
display: none
. My workaround to solve this issue was to hide the text usingopacity: 0
andopacity: 1
to show the text. Probably this method is not applicable for all scenarios but it can work for some of them.I support Mauro’s comment : this double-click situation happens only when an element inside the link changes from display none to another value of display on hover.
An easy workaround is to use any other way of hiding/revealing this element (opacity, visibility, position…) instead of display none.
Anyway I agree that those elements revealed shouldn’t be essential ones, and the interface should be understandable without them, so that users not using a mouse aren’t left helpless.
Why is it now not so much of a problem? Have mobile browser shortened the delay or found a workaround for that?
on [my website] I was able to solve this issue by instead of using width and height to scale a pseudo element inside a link, I transform:scale(0); then on hover change it to transform:scale(1);
This partially solved my issue as the animation starts but iOS doesn’t let it finish before loading the next page. So I added a media query to the buttons that sets transition:all 0s; and interestingly the animation actually works on iOS albeit it’s shorter but it’s not actually 0s it’s just shorter than desktop.
Hope it helps someone, see the link I posted and inspect the buttons to see what I’m referring to.
Andreas Deuschlinger wrote in to say that he found that
width
also can trigger it. As in setting a child element or pseudo element towidth: 0
is similar todisplay: none
orvisibility: hidden
and triggers this “bug”. I gotta imagineheight: 0
is a problem as well. Would be curious to see if kicking the element off the page via absolute positioning would as well (I suspect not.)