The Annoying Mobile Double-Tap Link Issue

Avatar of Chris Coyier
Chris Coyier on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

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.