How to prevent sticky hover effects for buttons on touch devices

131,621

Solution 1

Since this part of CSS Media Queries Level 4 has now been widely implemented since 2018, you can use this:

@media (hover: hover) {
    button:hover {
        background-color: blue;
    }
}

Or in English: "If the browser supports proper/true/real/non-emulated hovering (e.g. has a mouse-like primary input device), then apply this style when buttons are hovered over."

For browsers that do not have this implemented (or didn't at the time of this original answer), I wrote a polyfill to deal with this. Using it, you can transform the above futuristic CSS into:

html.my-true-hover button:hover {
    background-color: blue;
}

(A variation on the .no-touch technique) And then using some client-side JavaScript from the same polyfill that detects support for hovering, you can toggle the presence of the my-true-hover class accordingly:

$(document).on('mq4hsChange', function (e) {
    $(document.documentElement).toggleClass('my-true-hover', e.trueHover);
});

Solution 2

You can remove the hover state by temporarily removing the link from the DOM. See http://testbug.handcraft.com/ipad.html


In the CSS you have:

:hover {background:red;}

In the JS you have:

function fix()
{
    var el = this;
    var par = el.parentNode;
    var next = el.nextSibling;
    par.removeChild(el);
    setTimeout(function() {par.insertBefore(el, next);}, 0)
}

And then in your HTML you have:

<a href="#" ontouchend="this.onclick=fix">test</a>

Solution 3

This is a common problem with no perfect solution. Hover behavior is useful with a mouse and mostly detrimental with touch. Compounding the problem are devices which support touch and mouse (simultaneously, no less!) like the Chromebook Pixel and Surface.

The cleanest solution I've found is to only enable hover behavior if the device isn't deemed to support touch input.

var isTouch =  !!("ontouchstart" in window) || window.navigator.msMaxTouchPoints > 0;

if( !isTouch ){
    // add class which defines hover behavior
}

Granted, you lose hover on devices which may support it. However, sometimes hover impacts more than the link itself, e.g. perhaps you want to show a menu when an element is hovered. This approach allows you to test for the existence of touch and perhaps conditionally attach a different event.

I've tested this on the iPhone, iPad, Chromebook Pixel, Surface, and a variety of Android devices. I can't guarantee that it will work when a generic USB touch input (such as a stylus) is added to the mix.

Solution 4

You can override the hover effect for devices that don't support hover. Like:

.my-thing {
    color: #BADA55;
}

.my-thing:hover {
    color: hotpink;
}

@media (hover: none) {
    .my-thing {
        color: #BADA55;
    }
}

Tested and verified on iOS 12

Hat tip to https://stackoverflow.com/a/50285058/178959 for pointing this out.

Solution 5

From 2020 You can add hover styles inside media query

@media (hover: hover) and (pointer: fine) {
    /* css hover class/style */
}

This media query indicates that styles will work on browsers that not emulate :hover so it will NOT work on touch browsers.

Share:
131,621

Related videos on Youtube

Chris
Author by

Chris

Updated on November 11, 2021

Comments

  • Chris
    Chris over 2 years

    I have created a carousel with a previous and a next button that are always visible. These buttons have a hover state, they turn blue. On touch devices, like iPad, the hover state is sticky, so the button stays blue after tapping it. I don't want that.

    • I could add a no-hover class ontouchend for each button, and make my CSS like this: button:not(.no-hover):hover { background-color: blue; } but that's probably quite bad for performance, and doesn't handle devices like the Chromebook Pixel (which has both a touchscreen and a mouse) correctly.

    • I could add a touch class to the documentElement and make my CSS like this: html:not(.touch) button:hover { background-color: blue; } But that also doesn't work right on devices with both touch and a mouse.

    What I would prefer is removing the hover state ontouchend. But it doesn't seem like that is possible. Focusing another element doesn't remove the hover state. Tapping another element manually does, but I can't seem to trigger that in JavaScript.

    All the solutions I have found seem imperfect. Is there a perfect solution?

    • dasfdsa
      dasfdsa over 3 years
    • Chris
      Chris over 2 years
      That's a pretty nice solution, @dasfdsa! However, it's not a solution for devices that support both a touchscreen and a mouse.
  • Chris
    Chris almost 11 years
    But I would like to maintain the hover effect for mouse users.
  • Chris
    Chris almost 11 years
    Wow! I should have known Sjoerd would have the answer. :) There is one small downside though: after a click with a mouse, the hover effect also disappears. Normally that doesn't happen, so it looks a bit weird. Using ontouchend instead of onclick doesn't work though. :(
  • G-Cyrillus
    G-Cyrillus almost 11 years
    :hover and :active can receive same CSS, it's on :focus you have the problem. Actually, if you set background-color via onfocus, color style remains once focus has gone. you need a reset on onlur as well to restore defaut bg
  • Sjoerd Visscher
    Sjoerd Visscher almost 11 years
    @Chris Good point, I changed the example to set the onclick handler in the ontouchend event.
  • 2540625
    2540625 over 9 years
    Please consider adding minimal demonstrative code to your answer. Thanks! stackoverflow.com/help/how-to-answer
  • Sjoerd Visscher
    Sjoerd Visscher over 9 years
    @janaspage It's right there in the link. I've turned this into a community wiki answer, so if you want to put it here, go right ahead!
  • Darren Cook
    Darren Cook over 9 years
    @SjoerdVisscher I've pasted it in. StackOverflow likes the code to be in the answer, as links can go away. (And in this case it required not just click through, but then viewing the source, and working out which bits are the technique in question.)
  • Kevin Borders
    Kevin Borders over 9 years
    Using setTimeout sometimes causes flashing. The code appears to work still without it, so it can be removed.
  • morrisbret
    morrisbret over 9 years
    This is the best answer IMO BUT the example is incorrect. It's showing the same hover state for touch and non-touch devices. It should only apply the hover state if .no-touch is present on the html tag. Otherwise, this answer gets my stamp of approval.
  • dudewad
    dudewad about 9 years
    The problem is that now that mouse-enabled devices that have touch screens are all over the place, you can't really rely on this method anymore. However I can't see much of another way to do it... this is quite a dilemma
  • Rodney
    Rodney almost 9 years
    @KevinBorders yes on some devices the time delay between the removal and reinsert of the element can be very noticable. Unfortunately, I found on my android 4.4 device that doing this without setTimeout didn't work.
  • Ethan
    Ethan about 8 years
    @DarrenCook But is this a good idea to remove the element and re-add it? I guess this will cause 'janking' and conflict with 60fps butter smooth needs of the app.
  • Will Lanni
    Will Lanni about 8 years
    You could improve this answer using an on() instead of a click(). By removing the element from the DOM and re-attaching it, I lost my click events. When I rewrote my function with the on, binding to the element, then removing and adding worked. For example: `$('body').on('click', '#elementwithhover',function() { // clone, insertafter, remove }); I'll vote this up if you make that change.
  • Jannik
    Jannik almost 8 years
    Awesome, this worked for me, unlike the community wiki/Darren Cook answer.
  • Anthony Astige
    Anthony Astige almost 8 years
    This fix, when inserted into my application, appears to work on my Nexus 5 running android 4.4.2 (with 100ms delay) in Chrome but not my iOS iPod touch in Safari
  • Vernard Sloggett
    Vernard Sloggett almost 8 years
    the clone true preserves the click event on the new elemenet taking the place of the element with the stuck hover. the original element is being removed from the dom after its cloned.
  • Kevin Lee
    Kevin Lee almost 8 years
    For your code snippet, the square still remains pink after I touched it. I guess you didn't really solve the problem asked in this question?
  • ragamufin
    ragamufin over 7 years
    Thanks! For my purposes this seems to be supported in most browser caniuse.com/#search=media%20queries and works brilliantly, thanks!
  • Lorenzo Polidori
    Lorenzo Polidori over 7 years
    The idea is interesting, but I found it a bit jerky and not widely supported on all touch devices. I would prefer a less invasive solution based on touch support detection and pure css like this stackoverflow.com/a/39787268/885464
  • Lorenzo Polidori
    Lorenzo Polidori over 7 years
    Good answer. This is a similar solution: stackoverflow.com/a/39787268/885464
  • theRyanMark
    theRyanMark over 7 years
    This has broken for me in iOS10 for me. Has anyone else had the same issue?
  • CherryDT
    CherryDT over 7 years
    You need to look at caniuse.com/#feat=css-media-interaction instead and you'll see it's not supported in Firefox and IE11 :( so you need the polyfill
  • lowtechsun
    lowtechsun over 7 years
    This is the best way I find, plus if you increase the timer to 1000ms you can also cover the long-press, see here. Great stuff!
  • drskullster
    drskullster about 7 years
    Didn't work for me, but this did : document.body.className = 'ontouchstart' in window ? '' : 'hover';
  • xHocquet
    xHocquet almost 7 years
    This solution does not work. First, you used the wrong method to invoke a an event, you should use .trigger(). Second, doesn't work on mobile safari either way.
  • Kaloyan Stamatov
    Kaloyan Stamatov over 6 years
    awesome bro, 10x
  • Jusid
    Jusid over 6 years
    You need to call hoverTouchUnstick() once from the global ontouchstart event handler. And this solution would work perfectly.
  • Gavin
    Gavin about 6 years
    I suppose one way to do it would be to use both Modernizr as well as a library such as mobile-detect.js to make sure it's either a phone or a tablet.
  • amoebe
    amoebe over 5 years
    It seems the polyfill is no longer supported (repo is archived) and it requires jQuery...
  • cspolton
    cspolton about 5 years
    Doesn't work on hybrid devices, e.g. Windows touchscreen PCs.
  • Trevor Burnham
    Trevor Burnham almost 5 years
    This is now widely supported in mobile browsers, and works like a charm. I think it should be the accepted answer.
  • phil917
    phil917 over 4 years
    Hours of searching for a fix and this finally provided one. Thanks a million!
  • funkju
    funkju over 4 years
    This is the more modern solution, no javascript or DOM manipulating required, has full browser support (except IE) and should be the accepted answer.
  • Shivam
    Shivam about 4 years
    You are the saviour man thanks a lot this worked perfectly for mobile devices
  • Jensei
    Jensei about 4 years
    Doesn't this also remove hover effect for touchscreen devices with mouse/trackpad?
  • Hmerman6006
    Hmerman6006 about 4 years
    This is definitely the easiest to implement and best answer. Works in Firefox on Android Mobile and PC. Not sure about dual touch and mouse devices.
  • terrymorse
    terrymorse about 4 years
    @Jensei Possibly. Hover rules are removed if the device matches @media (hover:none), or when the user first touches the screen. You can try it out on the live demo page to be sure.
  • Banning
    Banning about 4 years
    In the end this did not work for me... my case I would have to clone an entire ES6 which is extended and several event listeners and It just started becoming to complicated to solve such a tiny thing.
  • dasfdsa
    dasfdsa over 3 years
    also check: css-tricks.com/…
  • Coderer
    Coderer over 2 years
    This doesn't actually fix the problem for dual input devices. (I'm writing this from a Surface Book 2.) You can move the mouse/touchpad after pressing a button via touch, but it's a shoddy fix. We really need a pseudoclass that's true for hover-with-a-pointer only.
  • Coderer
    Coderer over 2 years
    This also does not address dual-mode devices (touchscreen laptops etc). hover: none won't be true if the browser has both touch and pointer inputs.
  • Coderer
    Coderer over 2 years
    This does not fix the problem for multi-input devices. The no-touch class is applied on my Surface Book 2 (trackpad + touchscreen), but the :hover rule sticks in your fiddle link.
  • Coderer
    Coderer over 2 years
    This works on touch-only browsers, i.e. touchscreens that do not also have a regular pointer available. It does not fix the problem for e.g. touchscreen laptops.
  • Coderer
    Coderer over 2 years
    Thanks, I didn't use the interval-based approach but your information about preventDefault at the end was incredibly helpful -- I haven't seen this posted anywhere else!
  • Chris
    Chris over 2 years
    Nice solution! Doesn't work for devices that support both touch and mouse though.