'A click in iOS Safari triggers a "hover state" on element underneath where you tapped

On iOS Safari 11, if I have a <div> positioned over an element that has a :hover effect, and the <div> has an event that makes it disappear when clicked, then my link "underneath" gets the hover effect applied after the element is removed from the DOM.

See below for an animated GIF of what I'm talking about:

iOS click through

I've given the button a see-through background so you can see the link behind it. When I tap the button in on a spot where the link is not located, the link (i.e. Some link) stays blue and does not change to its hover state of red.

However, when I tap the div in a spot that happens to be directly over the link, after the div is removed from the DOM, the link gets its hover state applied.

Clicking the link after each of these correctly triggers its on.click event (an alert window).

I do not see this issue on android on Chrome (see example below):

Android click through working

Below you'll also find the sample HTML/CSS/JS I used; the setup is pretty simple.

I'd like to have iOS act in the same way Android Chrome does: that is, clicking on an element that is immediately removed from the DOM should not trigger a :hover state for an element immediately behind it.

var button = document.querySelector(".button");
button.addEventListener("click", function() {
  button.parentNode.removeChild(button);
});

var link = document.querySelector('a');
link.addEventListener("click", function() {
  window.alert('clicked!');
});
a:link    { color: blue }
a:visited { color: blue }
a:hover   { color: red }
a:active  { color: green }

.button {
    background: rgba(100, 0, 0, .4);
    position: absolute;
  
    top: 2px;
    left: 2px;
    z-index: 1;

    width: 100px;
    height: 100px;
    line-height: 100px;

    text-align: center;
    border-radius: 5px;
}

.button:hover {
    background: rgba(0, 100, 0, .4);
    cursor: pointer;
}
<a href="#">Some link</a>
<div class="button">Click me!</div>


Solution 1:[1]

If you have option to disable hover then you can do that with bit of hack.

First add this line in "DOMContentLoaded" event handler.

var canHover = !(matchMedia('(hover: none)').matches);
if (canHover) {
  document.body.classList.add('can-hover');
}

It will add .can-hover class to body of your html document.

Then just edit a:hover rule to match this instead

your css rule:

a:hover {
  color: red;
}

should be:

.can-hover a:hover {
  color: red;
}

this should prevent devices with touch screen to apply hover style to any anchor.

Solution 2:[2]

Have you tried using a media query to test for :hover capability?

See https://drafts.csswg.org/mediaqueries/#hover

Solution 3:[3]

...it can be clearly seen that this is the intended behavior on iOS even when swiping on a list of hover-able items.

If what you are trying to avoid is just the hover effect, here's what I would do. I would get the device using a JS function, apply a class on the body tag and remove any hover effects as needed.

Javascript:

function isAppleDevice() {
  return (
    (navigator.userAgent.toLowerCase().indexOf("ipad") > -1) ||
    (navigator.userAgent.toLowerCase().indexOf("iphone") > -1) ||
    (navigator.userAgent.toLowerCase().indexOf("ipod") > -1)
   );
}
if (isAppleDevice() {
  $('body').addClass('is_apple')
}

CSS:

.is_apple a:hover {
  // same styling as not hovering
}

Solution 4:[4]

This seems to be intended behaviour in Safari. Apple's documentation says that single-finger taps are handled as mouse events, and shows which events are raised during a tap. There's a mouseover at the start of the tap, but a mouseout is only triggered when you click on something else. This implies that Safari assumes that the pointer stays in the same place until the next tap, which would mean it makes sense for the lower element to be in the hover state.

I tried your code in Chrome on Android, and Chrome and Firefox on Windows (I don't have an Apple device). In all three cases, when I click on the <div> using a mouse (over the link), the hover style is applied to the link afterwards, unless I move the cursor away from the link before I release the mouse. If I tap on the <div> with my finger on the touchscreen, the link does not get the hover style.

Firefox and Chrome seem to be consistent with each other for touch and mouse, but Safari seems (by design) to treat the touchscreen as a mouse: it behaves the same way for a touch press as the Chrome & Firefox do for a mouse click.

I don't this that this is a bug you need to fix or work around. Unless you know all your users will be using a specific hardware/browser combination, you probably need to plan for the eventuality that they could use a mouse, a touchscreen, or perhaps both at the same time; and design a UI that works for all these cases. My Windows laptop and Android tablet both have mousepads as well as touchscreens, and I sometimes use a USB OTG mouse with my Android phone.

You could also look at the Pointer Events API, which works in Chrome, IE and Edge (not to be confused with the pointer-events CSS property mentioned by @erier). As the API deals with events rather than CSS, this won't directly help get your styling right, but shows the efforts going on to make browsers react consistently to different types of input device.

Solution 5:[5]

Try the new media query method to test whether the user's primary input mechanism can hover over elements.

@media (hover) {
  // Your hover effects
  a:hover {
    color: red;
  }
}

Docs: hover - CSS: Cascading Style Sheets | MDN

Solution 6:[6]

This seems to be an inherent design decision in iOS (or maybe the Chrome way is the design decision, made by them; can't tell). It seems iOS is designed to be "mouse-like". It even throws "mouseovers" on webpages even on their touch-only devices. I've been trying to find a way to determine if the user has a mouse so you can disable hover effects if there is one, but I can't find anything that will work correctly.

I think Windows does the same thing with touch, too. Touching a point effectively moves the mouse pointer there and clicks, leaving the mouse pointer.

You may just have to change your design if this is a deal-breaker.

Solution 7:[7]

I had a similar issue which I have solved by adding pointer events none for a split second just before the element is removed and then setting it back again after it's gone.

document.addEventListener("DOMContentLoaded", function ready() {
  var button = document.querySelector(".button");
  button.addEventListener("click", function() {
    document.querySelector('a').classList.add("no-click");
    button.parentNode.removeChild(button);
    setTimeout(function() {
        document.querySelector('a').classList.remove("no-click");
    },1);
  });
  
  document.querySelector('a').addEventListener("click", function() {
    window.alert('clicked!');
  });
});
a:link    { color: blue }
a:visited { color: blue }
a:hover   { color: red }
a:active  { color: green }

.button {
    background: rgba(100, 0, 0, .4);
    position: absolute;
  
    top: 2px;
    left: 2px;
    z-index: 1;

    width: 100px;
    height: 100px;
    line-height: 100px;

    text-align: center;
    border-radius: 5px;
}

.button:hover {
    background: rgba(0, 100, 0, .4);
    cursor: pointer;
}
.no-click {
    pointer-events: none;
}
<a href="#">Some link</a>
<div class="button">Click me!</div>

Solution 8:[8]

I think this hack can work.

You have to disable hover with a class, and when the top div clicked, add an event listener that will remove the hover-blocking class upon mouse move.

document.addEventListener("DOMContentLoaded", function ready() {
  var button = document.querySelector(".button");
  button.addEventListener("click", function() {
    button.parentNode.removeChild(button);
    var body = document.querySelector("body");
    body.addEventListener("mousemove", function() {
            var someLink = document.querySelector(".some-link");
            someLink.classList.remove("no-hover");
        });
  });
  
  document.querySelector('a').addEventListener("click", function() {
    window.alert('clicked!');
  });
});
a:link    { color: blue }
a:visited { color: blue }
a:hover   { color: red }
a:active  { color: green }

a.no-hover:hover { color: blue }

.button {
    background: rgba(100, 0, 0, .4);
    position: absolute;
  
    top: 2px;
    left: 2px;
    z-index: 1;

    width: 100px;
    height: 100px;
    line-height: 100px;

    text-align: center;
    border-radius: 5px;
}

.button:hover {
    background: rgba(0, 100, 0, .4);
    cursor: pointer;
}
<a href="#" class="some-link no-hover">Some link</a>
<div class="button">Click me!</div>

Solution 9:[9]

Using pointer-events might help here.

document.addEventListener("DOMContentLoaded", function ready() {
  var button = document.querySelector(".button");
  button.addEventListener("click", function() {
    button.parentNode.removeChild(button);
  });
  
  document.querySelector('a').addEventListener("click", function() {
    window.alert('clicked!');
  });
});
a {pointer-events: none;}
a:link    { color: blue; pointer-events: auto; }
a:visited { color: blue }
a:hover   { color: red; pointer-events: auto; }
a:active  { color: green; pointer-events: auto; }

.button {
    background: rgba(100, 0, 0, .4);
    position: absolute;
  
    top: 2px;
    left: 2px;
    z-index: 1;

    width: 100px;
    height: 100px;
    line-height: 100px;

    text-align: center;
    border-radius: 5px;
}

.button:hover {
    background: rgba(0, 100, 0, .4);
    cursor: pointer;
}
<a href="#">Some link</a>
<div class="button">Click me!</div>

Solution 10:[10]

I don't know if this would work in your production code, but it should for this example.

You could spit your hover rules out to a class, and apply this class to the link in your button removal callback, after you've zapped it from the DOM.

If that still doesn't work, you could add the class in an onmouseover callback. This would only work after the button was removed and after exiting (if relevant) and re-entering the link.

It's all a bit hacky, but it would allow you to taylor the behaviour to suit in either browser / OS.

Solution 11:[11]

This is a problem with browsers. hover state exists everywhere even when it shouldn't.

I would add a class of .no-touch to the body and style only those.

if (!("ontouchstart" in document.documentElement)) {
    document.body.classList.add('no-touch')
}

Here's everything together

if (!("ontouchstart" in document.documentElement)) {
    document.body.classList.add('no-touch')
}
document.addEventListener("DOMContentLoaded", function ready() {
  var button = document.querySelector(".button");
  button.addEventListener("click", function(e) {
    button.parentNode.removeChild(button);
  });
  
  document.querySelector('a').addEventListener("click", function() {
    window.alert('clicked!');
  });
});
a:link    { color: blue }
a:visited { color: blue }
.no-touch a:hover   { color: red }
a:active  { color: green }

.button {
    background: rgba(100, 0, 0, .4);
    position: absolute;
  
    top: 2px;
    left: 2px;
    z-index: 1;

    width: 100px;
    height: 100px;
    line-height: 100px;

    text-align: center;
    border-radius: 5px;
}

.button:hover {
    background: rgba(0, 100, 0, .4);
    cursor: pointer;
}
<a href="#">Some link</a>
<div class="button">Click me!</div>

I appreciate this might not be the answer you are looking for but it will definitely keep it consistent between Android and iOS. Will potentially help others with the same problem in the future.

Solution 12:[12]

MouseEvent/releatedTarget document says

For events with no secondary target, relatedTarget returns null.

This is the case when the mouseover or mouseenter is triggered by click or touch.

someElement.addEventListener(
  "mouseenter",
  (e)=>{
    e.relatedTarget // null if triggered by click or touch, 
                    // otherwise it refers to the element the pointer came from.
  })