Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent sticky hover effects for buttons on touch devices

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?

like image 573
Chris Avatar asked Jun 21 '13 11:06

Chris


People also ask

How do you remove ignore hover CSS style on touch devices?

You can remove all the CSS rules containing :hover using Javascript. This has the advantage of not having to touch CSS and being compatible even with older browsers. Limitations: stylesheets must be hosted on the same domain (that means no CDNs).

How do I turn off hover effect?

To remove the CSS hover effect from a specific element, you can set the pointer-events property of the element (the hover behavior of which you want to disable) to “none”.

What is sticky hover?

While this is overall a good thing, it leads to what's known as the "sticky hover" issue on these devices, where the :hover style stays with the element the user just tapped on until he/she taps again elsewhere in the document, or in some circumstances, reloads the page before the effect is dismissed.

How do you simulate hover on touch devices?

To answer your main question: “How do I simulate a hover with a touch in touch enabled browsers?” Simply allow 'clicking' the element (by tapping the screen), and then trigger the hover event using JavaScript. var p = document.


17 Answers

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);
});
like image 198
cvrebert Avatar answered Oct 09 '22 21:10

cvrebert


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>
like image 43
3 revs, 2 users 88% Avatar answered Oct 09 '22 20:10

3 revs, 2 users 88%


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.

like image 20
Tim M. Avatar answered Oct 09 '22 21:10

Tim M.


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.

like image 43
OrganicPanda Avatar answered Oct 09 '22 21:10

OrganicPanda


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.

like image 31
Dawid Świtoń-Maniakowski Avatar answered Oct 09 '22 21:10

Dawid Świtoń-Maniakowski


With Modernizr you can target your hovers specifically for no-touch devices:

(Note: this doesn't run on StackOverflow's snippet system, check the jsfiddle instead)

/* this one is sticky */
#regular:hover, #regular:active {
  opacity: 0.5;
}

/* this one isn't */
html.no-touch #no-touch:hover, #no-touch:active {
  opacity: 0.5;
}

Note that :active doesn't need to be targeted with .no-touch because it works as expected on both mobile and desktop.

like image 22
rachel Avatar answered Oct 09 '22 20:10

rachel


$("#elementwithhover").click(function() { 
  // code that makes element or parent slide or otherwise move out from under mouse. 

  $(this).clone(true).insertAfter($(this));
  $(this).remove();
});
like image 32
Vernard Sloggett Avatar answered Oct 09 '22 21:10

Vernard Sloggett


From 4 ways to deal with sticky hover on mobile: Here's a way to dynamically add or remove a "can touch" class to the document based on the current input type of the user. It works with hybrid devices as well where the user may be switching between touch and a mouse/trackpad:

<script>

;(function(){
    var isTouch = false //var to indicate current input type (is touch versus no touch) 
    var isTouchTimer 
    var curRootClass = '' //var indicating current document root class ("can-touch" or "")

    function addtouchclass(e){
        clearTimeout(isTouchTimer)
        isTouch = true
        if (curRootClass != 'can-touch'){ //add "can-touch' class if it's not already present
            curRootClass = 'can-touch'
            document.documentElement.classList.add(curRootClass)
        }
        isTouchTimer = setTimeout(function(){isTouch = false}, 500) //maintain "istouch" state for 500ms so removetouchclass doesn't get fired immediately following a touch event
    }

    function removetouchclass(e){
        if (!isTouch && curRootClass == 'can-touch'){ //remove 'can-touch' class if not triggered by a touch event and class is present
            isTouch = false
            curRootClass = ''
            document.documentElement.classList.remove('can-touch')
        }
    }

    document.addEventListener('touchstart', addtouchclass, false) //this event only gets called when input type is touch
    document.addEventListener('mouseover', removetouchclass, false) //this event gets called when input type is everything from touch to mouse/ trackpad
})();

</script>
like image 42
coco puffs Avatar answered Oct 09 '22 20:10

coco puffs


I was going to post my own solution, but checking if someone already posted it, I found that @Rodney almost did it. However, he missed it one last crucial that made it uniseful, at least in my case. I mean, I too took the same .fakeHover class addition / removing via mouseenter and mouseleave event detection, but that alone, per se, acts almost exactly like "genuine" :hover. I mean: when you tap on a element in your table, it won't detect that you have "leaved" it - thus keeping the "fakehover" state.

What I did was simply to listen on click, too, so when I "tap" the button, I manually fire a mouseleave.

Si this is my final code:

.fakeHover {
    background-color: blue;
}


$(document).on('mouseenter', 'button.myButton',function(){
    $(this).addClass('fakeHover');
});

$(document).on('mouseleave', 'button.myButton',function(){
    $(this).removeClass('fakeHover');
});

$(document).on('button.myButton, 'click', function(){
    $(this).mouseleave();
});

This way you keep your usual hover functionality when using a mouse when simply "hovering" on your buttons. Well, almost all of it: the only drawback somehow is that, after clicking on the button with the mouse, it wont be in hover state. Much like if you clicked and quickly took the pointer out of the button. But in my case I can live with that.

like image 40
Pere Avatar answered Oct 09 '22 20:10

Pere


This is what I have come up with so far after studying the rest of the answers. It should be able to support touch-only, mouse-only or hybrid users.

Create a separate hover class for the hover effect. By default, add this hover class to our button.

We do not want to detect the presence of touch support and disable all hover effects from the very beginning. As mentioned by others, hybrid devices are gaining popularity; people may have touch support but want to use a mouse and vice versa. Therefore, only remove the hover class when the user actually touches the button.

The next problem is, what if the user wants to go back to using a mouse after touching the button? To solve that, we need to find an opportune moment to add back the hover class which we have removed.

However, we cannot add it back immediately after removing it, because the hover state is still active. We may not want to destroy and recreate the entire button as well.

So, I thought of using a busy-waiting algorithm (using setInterval) to check the hover state. Once the hover state is deactivated, we can then add back the hover class and stop the busy-waiting, bringing us back to the original state where the user can use either mouse or touch.

I know busy-waiting isn't that great but I'm not sure if there is an appropriate event. I've considered to add it back in the mouseleave event, but it was not very robust. For example, when an alert pops up after the button is touched, the mouse position shifts but the mouseleave event is not triggered.

var button = document.getElementById('myButton');

button.ontouchstart = function(e) {
  console.log('ontouchstart');
  $('.button').removeClass('button-hover');
  startIntervalToResetHover();
};

button.onclick = function(e) {
  console.log('onclick');
}

var intervalId;

function startIntervalToResetHover() {
  // Clear the previous one, if any.
  if (intervalId) {
    clearInterval(intervalId);
  }
  
  intervalId = setInterval(function() {
    // Stop if the hover class already exists.
    if ($('.button').hasClass('button-hover')) {
      clearInterval(intervalId);
      intervalId = null;
      return;
    }
    
    // Checking of hover state from 
    // http://stackoverflow.com/a/8981521/2669960.
    var isHovered = !!$('.button').filter(function() {
      return $(this).is(":hover");
    }).length;
    
    if (isHovered) {
      console.log('Hover state is active');
    } else {
      console.log('Hover state is inactive');
      $('.button').addClass('button-hover');
      console.log('Added back the button-hover class');
      
      clearInterval(intervalId);
      intervalId = null;
    }
  }, 1000);
}
.button {
  color: green;
  border: none;
}

.button-hover:hover {
  background: yellow;
  border: none;
}

.button:active {
  border: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id='myButton' class='button button-hover'>Hello</button>

Edit: Another approach I tried is to call e.preventDefault() within ontouchstart or ontouchend. It appears to stop the hover effect when the button is touched, but it also stops the button click animation and prevents the onclick function from being called when the button is touched, so you have to call those manually in the ontouchstart or ontouchend handler. Not a very clean solution.

like image 45
Kevin Lee Avatar answered Oct 09 '22 21:10

Kevin Lee


It was helpful for me: link

function hoverTouchUnstick() {
    // Check if the device supports touch events
    if('ontouchstart' in document.documentElement) {
        // Loop through each stylesheet
        for(var sheetI = document.styleSheets.length - 1; sheetI >= 0; sheetI--) {
            var sheet = document.styleSheets[sheetI];
            // Verify if cssRules exists in sheet
            if(sheet.cssRules) {
                // Loop through each rule in sheet
                for(var ruleI = sheet.cssRules.length - 1; ruleI >= 0; ruleI--) {
                    var rule = sheet.cssRules[ruleI];
                    // Verify rule has selector text
                    if(rule.selectorText) {
                        // Replace hover psuedo-class with active psuedo-class
                        rule.selectorText = rule.selectorText.replace(":hover", ":active");
                    }
                }
            }
        }
    }
}
like image 27
mineroot Avatar answered Oct 09 '22 21:10

mineroot


Add this JS code to your page:

document.body.className = 'ontouchstart' in document.documentElement ? '' : 'hover';

now in your CSS before every hover add the hover class like this:

.hover .foo:hover {}

If the device is touch, the body class will be empty, otherwise its class will be hover and the rules are applied!

like image 25
Ali Avatar answered Oct 09 '22 21:10

Ali


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.

This is the right starting point. Next step: apply/remove nohover class on following events (demo with jQuery)

buttonelement
 .on("touchend touchcancel",function(){$(this).addClass("nohover")})
 .on("touchstart mouseover",function({$(this).removeClass("nohover")});   

Notice: If You wish to apply other classes to the buttonelement, the :not(.nohover) in the CSS won't work anymore as expected. Than You have to add instead a separate definition with default value and !important tag to overwrite hover style: .nohover{ background-color: white !important}

This should even handle devices like the Chromebook Pixel (which has both a touchscreen and a mouse) correctly! And I don't think, that this a major performance killer...

like image 39
Sascha Hendel Avatar answered Oct 09 '22 20:10

Sascha Hendel


A solution that has worked for me:

html {
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

Add this code to your stylesheet.

I wanted to get rid of the grey background that appears on iOS Safari when a link is clicked. But it appears to do more. Now clicking a button (with a :hover pseudoclass!) gets opened right away! I only tested it on an iPad, I don't know if it'll work on other devices.

like image 45
Leon Vogler Avatar answered Oct 09 '22 22:10

Leon Vogler


I have nice solution that i would like to share. First you need to detect if user is on mobile like this:

var touchDevice = /ipad|iphone|android|windows phone|blackberry/i.test(navigator.userAgent.toLowerCase());

Then just add:

if (!touchDevice) {
    $(".navbar-ul").addClass("hoverable");
}

And in CSS:

.navbar-ul.hoverable li a:hover {
    color: #fff;
}
like image 24
Michał Reszka Avatar answered Oct 09 '22 21:10

Michał Reszka


I went through the similar problem, my application was compatible for all screen sizes had a hell lot of hover effects in the desktop-screen-size / mouse based devices , and later on I realised that touch based devices were causing a condition known as sticky-hover and it was an hurdle for the app to work properly for touch based device users.

We were making use of SCSS in our app. and I defined a mixin to take care of touch based device.

@mixin hover-support {
  @media not all and (pointer: coarse) {
    &:hover {
      @content;
    }
  }
}

and then I placed all my css classes under the below-mentioned snippet.

   @include hover-support() {
    // Your css-classes or css that you want to apply on those devices that support hover.
   }

For instance we had a class for animating an icon, and which used to trigger once we hover over icon as you can see it from css, but in touch based device it was getting affected by sticky hover-effect and then I placed it inside @include hover-support() in order to ensure that hover only applies to those devices that support hover.

@include hover-support() {
  .animate-icon {
    -webkit-transition: all 0.2s;
    transition: all 0.2s;
    &:hover {
      transform: scale(1.3);
      filter: brightness(85%);
      cursor: pointer;
    }
  }
}
like image 45
Divyanshu Rawat Avatar answered Oct 09 '22 20:10

Divyanshu Rawat


Here's some simple JavaScript code that doesn't require the developer to edit CSS or write any new CSS rules. I wrote this for Bootstrap buttons with class="btn", but it will work with any button that has a specific class name.

The steps are:

  1. determine if this is a touch device
  2. if it is, iterate over every CSS rule in document.styleSheets
  3. delete any rule containing both .btn and :hover

The elimination of all .btn :hover CSS rules ensures that there will be no visual hover effect on a button.

Step 1: Detect Touch Device

Check media query for presence of (hover: none):

    const hasMatchMedia = typeof window.matchMedia !== 'undefined';
    /**
     * determine if device is touch-capable
     * true - device is touch-capable
     * false - device is not touch-capable
     * null - unable to determine touch capability
     * @return {null|boolean}
     */
    const hasTouch = () => {
      if (hasMatchMedia) {
        return window.matchMedia('(hover: none)').matches;
      }
      return null;
    };

Step 2: Delete CSS Rules Containing `btn` and `:hover`

    /**
     * remove all CSS rules contaning both '.btn' and ':hover'
     * @return {number} count of rules deleted
     */
    function removeBtnHovers () {

      let rulesDeleted = 0;

      // recursively delete '.btn:hover' rules
      function recursiveDelete (rules, styleSheet) {

        if (typeof rules === 'undefined' ||
          typeof rules.length === 'undefined' ||
          rules.length <= 0) {
          return;
        }

        // iterate in reverse order,
        // deleting any rule containing both '.btn' and ':hover'
        const ruleLen = rules.length;
        for (let i = ruleLen - 1; i >= 0; i--) {
          const rule = rules[i];
          if (typeof rule.cssRules === 'undefined') {
            // a standard rule, evaluate it
            const cssText = rule.cssText;
            if (typeof cssText === 'string' &&
              cssText.includes('.btn') &&
              cssText.includes(':hover')) {
              styleSheet.deleteRule(i);
              rulesDeleted++;
            }
          } else {
            // rule contains cssRules, iterate over them
            recursiveDelete(rule.cssRules, rule);
          }
        }
      }

      // iterate over all style sheets in document
      for (const styleSheet of document.styleSheets) {
        let rules = styleSheet.cssRules;
        if (!rules) { continue; }
        recursiveDelete(rules, styleSheet);
      }
      return rulesDeleted;
    }

Complete code is on GitHub and npm.

Live demo at terrymorse.com.

like image 24
terrymorse Avatar answered Oct 09 '22 20:10

terrymorse