I'm writing a Web site that's meant to be used from both desktops and tablets. When it's being visited from a desktop, I want the clickable areas of the screen to light up with :hover effects (different background color, etc.) With a tablet, there's no mouse, so I don't want any hover effects.
The problem is, when I tap something on the tablet, the browser evidently has some kind of "invisible mouse cursor" that it moves to the location I tapped, and then leaves it there -- so the thing I just tapped lights up with a hover effect until I tap something else.
How can I get the hover effects when I'm using the mouse, but suppress them when I'm using the touchscreen?
In case someone was thinking of suggesting it, I don't want to use user-agent sniffing. The same device could have both a touchscreen and a mouse (maybe not so common today, but much more so in the future). I'm not interested in the device, I'm interested in how it's currently being used: mouse or touchscreen.
I already tried hooking the touchstart, touchmove, and touchend events and calling preventDefault() on all of them, which does suppress the "invisible mouse cursor" some of the time; but if I tap rapidly back and forth between two different elements, after a few taps it will start moving the "mouse cursor" and lighting up the hover effects anyway -- it's like my preventDefault isn't always honored. I won't bore you with the details unless necessary -- I'm not even sure that's the right approach to take; if anyone has a simpler fix, I'm all ears.
Edit: This can be reproduced with bog-standard CSS :hover, but here's a quick repro for reference.
<style>
.box { border: 1px solid black; width: 150px; height: 150px; }
.box:hover { background: blue; }
</style>
<div class="box"></div>
<div class="box"></div>
If you mouse over either of the boxes, it will get a blue background, which I want. But if you tap on either of the boxes, it will also get a blue background, which is the thing I'm trying to prevent.
I've also posted a sample here that does the above and also hooks jQuery's mouse events. You can use it to see that tap events will also fire mouseenter, mousemove and mouseleave.
I take it from your question that your hover effect changes the content of your page. In that case, my advice is to:
Add hover effects on touchstart and mouseenter.
Remove hover effects on mouseleave, touchmove and click.
Alternatively, you can edit your page that there is no content change.
Background
In order to simulate a mouse, browsers such as Webkit mobile fire the following events if a user touches and releases a finger on touch screen (like iPad) (source: Touch And Mouse on html5rocks.com):
touchstart
touchmove
touchend
300ms delay, where the browser makes sure this is a single tap, not a double tap
mouseover
mouseenter
Note: If a mouseover, mouseenter or mousemove event changes the page content, the following events are never fired.
mousemove
mousedown
mouseup
click
It does not seem possible to simply tell the webbrowser to skip the mouse events.
What's worse, if a mouseover event changes the page content, the click event is never fired, as explained on Safari Web Content Guide - Handling Events, in particular figure 6.4 in One-Finger Events. What exactly a "content change" is, will depend on browser and version. I've found that for iOS 7.0, a change in background color is not (or no longer?) a content change.
Solution Explained
To recap:
Add hover effects on touchstart and mouseenter.
Remove hover effects on mouseleave, touchmove and click.
Note that there is no action on touchend!
This clearly works for mouse events: mouseenter and mouseleave (slightly improved versions of mouseover and mouseout) are fired, and add and remove the hover.
If the user actually clicks a link, the hover effect is also removed. This ensure that it is removed if the user presses the back button in the web browser.
This also works for touch events: on touchstart the hover effect is added. It is '''not''' removed on touchend. It is added again on mouseenter, and since this causes no content changes (it was already added), the click event is also fired, and the link is followed without the need for the user to click again!
The 300ms delay that a browser has between a touchstart event and click is actually put in good use because the hover effect will be shown during this short time.
If the user decides to cancel the click, a move of the finger will do so just as normal. Normally, this is a problem since no mouseleave event is fired, and the hover effect remains in place. Thankfully, this can easily be fixed by removing the hover effect on touchmove.
That's it!
Note that it is possible to remove the 300ms delay, for example using the FastClick library, but this is out of scope for this question.
Alternative Solutions
I've found the following problems with the following alternatives:
browser detection: Extremely prone to errors. Assumes that a device has either mouse or touch, while a combination of both will become more and more common when touch displays prolifirate.
CSS media detection: The only CSS-only solution I'm aware of. Still prone to errors, and still assumes that a device has either mouse or touch, while both are possible.
Emulate the click event in touchend: This will incorrectly follow the link, even if the user only wanted to scroll or zoom, without the intention of actually clicking the link.
Use a variable to suppress mouse events: This set a variable in touchend that is used as a if-condition in subsequent mouse events to prevents state changes at that point in time. The variable is reset in the click event. See Walter Roman's answer on this page. This is a decent solution if you really don't want a hover effect on touch interfaces. Unfortunately, this does not work if a touchend is fired for another reason and no click event is fired (e.g. the user scrolled or zoomed), and is subsequently trying to following the link with a mouse (i.e on a device with both mouse and touch interface).
Further Reading
http://jsfiddle.net/macfreek/24Z5M/. Test the above solution for yourself in this sandbox.
http://www.macfreek.nl/memory/Touch_and_mouse_with_hover_effects_in_a_web_browser. This same answer, with a bit more background.
https://www.html5rocks.com/en/mobile/touchandmouse/. Great background article on html5rocks.com about touch and mouse in general.
https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html. Safari Web Content Guide - Handling Events. See in particular figure 6.4, which explains that no further events are fired after a content change during a mouseover or mousemove event.
How can I get the hover effects when I'm using the mouse, but suppress them when I'm using the touchscreen?
Maybe don't think of it so much as suppressing hover effects for touchscreens, but as adding hover effects for mouse events?
If you want to keep the :hover effects in your CSS you could specify different styles for different media:
#media screen { /* hover styles here */ }
#media handheld { /* non-hover styles here */ }
Except that unfortunately there are plenty of mobile devices that ignore this and just use the screen rules. Fortunately a lot of newer mobile/tablet browsers do support some fancier media queries:
#media screen and (max-width:800px) { /* non-hover styles here */ }
So even if the "screen" or "handheld" part is ignored the "max-width" will do the trick for you. You could just assume that anything with a screen smaller than 800 pixels must be a tablet or phone, and not use hover effects. For the rare users who are using a mouse on a low resolution device they wouldn't see the hover effects but your site would be fine otherwise.
Further reading on media queries? There are plenty of articles about this online - here is one: http://www.alistapart.com/articles/return-of-the-mobile-stylesheet
If you shift the hover effects out of your CSS and apply them with JavaScript then you could bind specifically to mouse events, and/or again you could just make some assumptions just based on screen size with the worst-case "problem" being that some user who is using a mouse misses out on the hover effects.
I wrote the following JS for a recent project, which was a desktop/mobile/tablet site that has hover effects that shouldn't appear on-touch.
The mobileNoHoverState module below has a variable preventMouseover (initially declared as false), that is set to true when a user fires the touchstart event on an element, $target.
preventMouseover is then being set back to false whenever the mouseover event is fired, which allows the site to work as intended if a user is using both their touchscreen and mouse.
We know that mouseover is being triggered after touchstart because of the order that they are being declared within init.
var mobileNoHoverState = function() {
var hoverClass = 'hover',
$target = $(".foo"),
preventMouseover = false;
function forTouchstart() {
preventMouseover = true;
}
function forMouseover() {
if (preventMouseover === false) {
$(this).addClass(hoverClass);
} else {
preventMouseover = false;
}
}
function forMouseout() {
$(this).removeClass(hoverClass);
}
function init() {
$target.on({
touchstart : forTouchstart,
mouseover : forMouseover,
mouseout : forMouseout
});
}
return {
init: init
};
}();
The module is then instantiated further down the line:
mobileNoHoverState.init();
I really wanted a pure css solution to this myself, since sprinkling a weighty javascript solution around all of my views seemed like an unpleasant option. Finally found the #media.hover query, which can detect "whether the primary input mechanism allows the user to hover over elements." This avoids touch devices where "hovering" is more of an emulated action than a direct capability of the input device.
So for example, if I have a link:
Home
Then I can safely style it to only :hover when the device easily supports it with this css:
#media (hover: hover) {
.link:hover { /* hover styles */ }
}
While most modern browsers support interaction media feature queries, some popular browsers such as IE and Firefox do not. In my case this works fine, since I only intended to support Chrome on desktop and Chrome and Safari on mobile.
My solution is to add hover-active css class to the HTML tag,
and use it on the beginning of all the CSS selectors with :hover
and remove that class on the first touchstart event.
http://codepen.io/Bnaya/pen/EoJlb
JS:
(function () {
'use strict';
if (!('addEventListener' in window)) {
return;
}
var htmlElement = document.querySelector('html');
function touchStart () {
document.querySelector('html').classList.remove('hover-active');
htmlElement.removeEventListener('touchstart', touchStart);
}
htmlElement.addEventListener('touchstart', touchStart);
}());
HTML:
<html class="hover-active">
CSS:
.hover-active .mybutton:hover {
box-shadow: 1px 1px 1px #000;
}
What I've done to solve the same problem is to have a feature detection (I use something like this code), seeing if onTouchMove is defined, and if so I add the css class "touchMode" to the body, else i add "desktopMode".
Then every time some style effect only applies to a touch device, or only to a desktop the css rule is prepended with the appropriate class:
.desktopMode .someClass:hover{ color: red }
.touchMode .mainDiv { width: 100%; margin: 0; /*etc.*/ }
Edit: This strategy of course adds a few extra characters to your css, so If you're concerned about css size, you could search for the touchMode and desktopMode definitons and put them into different files, so you can serve optimized css for each device type; or you could change the class names to something much shorter before going to prod.
Right, I jst had a similar problem but managed to fix it with media queries and simple CSS. I'm sure I'm breaking some rules here, but it's working for me.
I basically had to take a massive application someone made, and make it responsive. They used jQueryUI and asked me not to tamper with any of their jQuery, so I was restricted to using CSS alone.
When I pressed one of their buttons in touchscreen mode, the hover effect woudld fire for a second before the button's action took effect. Here's how I fixed it.
#media only screen and (max-width:1024px) {
#buttonOne{
height: 44px;
}
#buttonOne:hover{
display:none;
}
}
In my project we solved this issue using https://www.npmjs.com/package/postcss-hover-prefix and https://modernizr.com/
First we post-process output css files with postcss-hover-prefix. It adds .no-touch for all css hover rules.
const fs = require("fs");
const postcss = require("postcss");
const hoverPrfx = require("postcss-hover-prefix");
var css = fs.readFileSync(cssFileName, "utf8").toString();
postcss()
.use(hoverPrfx("no-touch"))
.process(css)
.then((result) => {
fs.writeFileSync(cssFileName, result);
});
css
a.text-primary:hover {
color: #62686d;
}
becomes
.no-touch a.text-primary:hover {
color: #62686d;
}
At runtime Modernizr automatically adds css classes to html tag like this
<html class="wpfe-full-height js flexbox flexboxlegacy canvas canvastext webgl
no-touch
geolocation postmessage websqldatabase indexeddb hashchange
history draganddrop websockets rgba hsla multiplebgs backgroundsize borderimage
borderradius boxshadow textshadow opacity cssanimations csscolumns cssgradients
cssreflections csstransforms csstransforms3d csstransitions fontface
generatedcontent video audio localstorage sessionstorage webworkers
applicationcache svg inlinesvg smil svgclippaths websocketsbinary">
Such post-processing of css plus Modernizr disables hover for touch devices and enables for others. In fact this approach was inspired by Bootstrap 4, how they solve the same issue: https://v4-alpha.getbootstrap.com/getting-started/browsers-devices/#sticky-hoverfocus-on-mobile
You can trigger the mouseLeave event whenever you touch an element on touchscreen. Here is a solution for all <a> tags:
function removeHover() {
var anchors = document.getElementsByTagName('a');
for(i=0; i<anchors.length; i++) {
anchors[i].addEventListener('touchstart', function(e){
$('a').mouseleave();
}, false);
}
}
Iv'd found 2 solutions to the problem, which its implied that you detect touch with modernizr or something else and set a touch class on the html element.
This is good but not supported very well:
html.touch *:hover {
all:unset!important;
}
But this has a very good support:
html.touch *:hover {
pointer-events: none !important;
}
Works flawless for me, it makes all the hover effects be like when you have a touch on a button it will light up but not end up buggy as the initial hover effect for mouse events.
Detecting touch from no-touch devices i think modernizr has done the best job:
https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
EDIT
I found a better and simpler solution to this issue
How to determine if the client is a touch device
It might help to see your CSS, as it sounds like a rather weird issue. But anyway, if it is happening and all else is good, you could try shifting the hover effect to javascript (you could use jquery as well).
Simply, bind to the mouseover or better still mouseenter event and light up your element when the event fires.
Checkout the last example here: http://api.jquery.com/mouseover/, you could use something similar to log when the event fires and take it from there!
If you are happy to use JavaScript then you can use Modernizr in your page. When the page loads, a non-touch screen browser will have the class '.no-touch' added to the html tag, but for a touch screen browser, the html tag will have the class '.touch' added to the html tag.
Then it is simply a case of checking to see if the html tag has the no-touch class before deciding to add your mouseenter and mouseleave listeners.
if($('html').hasClass('no-touch')){
$('.box').on("mouseenter", function(event){
$(this).css('background-color','#0000ff')
});
$('.box').on("mouseleave", function(event){
$(this).css('background-color','')
});
}
For a touchscreen device the events will have no listeners so you will get no hover effect when you tap.
In a project I did recently, I solved this problem with jQuery's delegated events feature. It looks for certain elements using a jQuery selector, and adds/removes a CSS class to those elements when the mouse is over the element. It seems to work well as far as I've been able to test it, which includes IE10 on a touch-capable notebook running Windows 8.
$(document).ready(
function()
{
// insert your own selector here: maybe '.hoverable'?
var selector = 'button, .hotspot';
$('body')
.on('mouseover', selector, function(){ $(this).addClass('mouseover'); })
.on('mouseout', selector, function(){ $(this).removeClass('mouseover'); })
.on('click', selector, function(){ $(this).removeClass('mouseover'); });
}
);
edit: this solution does, of course, require that you alter your CSS to remove the ":hover" selectors, and contemplate in advance on which elements you want to be "hoverable".
If you have very many elements on your page (like several thousand) it may get a bit slow, though, because this solution catches events of three types on all elements in the page, and then does its thing if the selector matches. I named the CSS class "mouseover" instead of "hover", because I didn't want any CSS readers to read ":hover" where I wrote ".hover".
Here is my solution: http://jsfiddle.net/agamemnus/g56aw709/-- code below.
All one needs to do is to convert their ":hover" to ".hover"... that's it! The big difference between this and the rest is that this will also work on non-singular element selectors such as .my_class > *:hover {.
handle_css_hover_effects ()
function handle_css_hover_effects (init) {
var init = init || {}
var handle_touch_events = init.handle_touch_events || true
var handle_mouse_events = init.handle_mouse_events || true
var hover_class = init.hover_class || "hover"
var delay_preferences = init.delay_preferences || {touch: {add: 500, remove: 500}}
function default_handler (curobj, input_type, op) {
var hovered_element_selector = "*" + ((op == "add") ? ":" : ("." + hover_class))
var hovered_elements = Array.prototype.slice.call(document.body.querySelectorAll(hovered_element_selector))
var modified_list = []
while (true) {
if ((curobj == null) || (curobj == document.documentElement)) break
if (hovered_elements.indexOf(curobj) != -1) modified_list.push (curobj)
curobj = curobj.parentNode
}
function do_hover_change () {modified_list.forEach (function (curobj) {curobj.classList[op](hover_class)})}
if ((!delay_preferences[input_type]) || (!delay_preferences[input_type][op])) {
do_hover_change ()
} else {
setTimeout (do_hover_change, delay_preferences[input_type][op])
}
}
if (handle_mouse_events) {
document.body.addEventListener ('mouseover' , function (evt) {var curobj = evt.target; default_handler (curobj, "mouse", "add")})
document.body.addEventListener ('mouseout' , function (evt) {var curobj = evt.target; default_handler (curobj, "mouse", "remove")})
document.body.addEventListener ('click' , function (evt) {var curobj = evt.target; default_handler (curobj, "mouse", "remove")})
}
if (handle_touch_events) {
document.body.addEventListener ('touchstart', function (evt) {var curobj = evt.target; default_handler (curobj, "touch", "add")})
document.body.addEventListener ('touchend' , function (evt) {var curobj = evt.target; default_handler (curobj, "touch", "remove")})
document.body.addEventListener ('touchmove', function (evt) {
var curobj = evt.target
var hovered_elements = Array.prototype.slice.call(document.body.querySelectorAll("*:hover"))
var lastobj = null
evt = evt.changedTouches[0]
var elements_at_point = get_elements_at_point (evt.pageX, evt.pageY)
// Get the last element that isn't at the current point but is still hovered over, and remove only its hover attribute.
while (true) {
if ((curobj == null) || (curobj == document.documentElement)) break
if ((hovered_elements.indexOf(curobj) != -1) && (elements_at_point.indexOf(curobj) == -1)) lastobj = curobj
curobj = curobj.parentNode
}
if (lastobj == null) return
if ((!delay_preferences.touch) || (!delay_preferences.touch.remove)) {
lastobj.classList.remove(hover_class)
} else {
setTimeout (function () {lastobj.classList.remove(hover_class)}, delay_preferences.touch.remove)
}
function get_elements_at_point (x, y) {
var el_list = [], pe_list = []
while (true) {
var curobj = document.elementFromPoint(x, y)
if ((curobj == null) || (curobj == document.documentElement)) break
el_list.push (curobj); pe_list.push (curobj.style.pointerEvents)
curobj.style.pointerEvents = "none"
}
el_list.forEach (function (current_element, i) {current_element.style.pointerEvents = pe_list[i]})
return el_list
}
})
}
}
Include Modernizr on your page and set your hover states like this instead:
html.no-touchevents .box:hover {
background: blue;
}
Hello person from the future, you probably want to use the pointer and/or hover media query. The handheld media query was deprecated.
/* device is using a mouse or similar */
#media (pointer: fine) {
a:hover {
background: red;
}
}
.services-list .fa {
transition: 0.5s;
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
color: blue;
}
/* For me, #media query is the easiest way for disabling hover on mobile devices */
#media only screen and (min-width: 981px) {
.services-list .fa:hover {
color: #faa152;
transition: 0.5s;
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
/* You can actiate hover on mobile with :active */
.services-list .fa:active {
color: #faa152;
transition: 0.5s;
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
.services-list .fa-car {
font-size:20px;
margin-right:15px;
}
.services-list .fa-user {
font-size:48px;
margin-right:15px;
}
.services-list .fa-mobile {
font-size:60px;
}
<head>
<title>Hover effects on mobile browsers</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<div class="services-list">
<i class="fa fa-car"></i>
<i class="fa fa-user"></i>
<i class="fa fa-mobile"></i>
</div>
</body>
For example: https://jsfiddle.net/lesac4/jg9f4c5r/8/
You can use js. It should work as expected.
function myFunction(){
var x = document.getElementById("DIV");
x.style.backgroundColor="red";
x.style.cursor="pointer";
x.style.color="white"
}
function my2Function(){
var x = document.getElementById("DIV");
x.style.backgroundColor="white";
x.style.color="red"
}
.mydiv {
background-color: white;
color: red;
}
<div class = "mydiv" id="DIV" onmouseover="myFunction()" onmouseleave="my2Function()">
hi
</div>
Here my issue has been fixed(mouseenter & touch related issue in React js) by using
onMouseEnter={() => addHeaderClassName()} onMouseLeave={() => removeHeaderClassName()} onFocus={() => addHeaderClassName()} onBlur={() => removeHeaderClassName()}
the above mentioned "onMouseEnter & onMouseLeave" works for large device like as desktop where mouse event can be detected, on the otherhand "onFocus & onBlur" works on small device like as tablet & mobile where touch can be detected.
Here my issue has been fixed(mouseenter & touch related issue in React js) by using
onMouseEnter={() => addHeaderClassName()} onMouseLeave={() => removeHeaderClassName()} onFocus={() => addHeaderClassName()} onBlur={() => removeHeaderClassName()}
the above mentioned "onMouseEnter & onMouseLeave" works for large device like as desktop where mouse event can be detected, on the otherhand "onFocus & onBlur" works on small device like as tablet & mobile where touch can be detected.
Try this easy 2019 jquery solution, although its been around a while;
add this plugin to head:
src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js"
add this to js:
$("*").on("touchend", function(e) { $(this).focus(); }); //applies to all elements
some suggested variations to this are:
$(":input, :checkbox,").on("touchend", function(e) {(this).focus);}); //specify elements
$("*").on("click, touchend", function(e) { $(this).focus(); }); //include click event`
css: body { cursor: pointer; } //touch anywhere to end a focus`
Notes
place plugin before bootstrap.js, if applicable, to avoid affecting tooltips
only tested on iphone XR ios 12.1.12, and ipad 3 ios 9.3.5, using Safari or Chrome.
References:
https://code.jquery.com/ui/
https://api.jquery.com/category/selectors/jquery-selector-extensions/
Related
I have the following drop-down :
<select>
<option></option>
<option>Closed</option>
<option>Open</option>
</select>
with the associated style:
select {
font-family: Cursive;
width:200px;
position: relative;
z-index: 100;
padding-right: 25px;
}
My problem is that the drop-down is moving upward on IE 11:
Where as on chrome it is working fine.
Any idea ?
Like mentioned in the comments, select menus are very browser specific and hard to style. Some even send the options into the twilight zone where they are seemingly not even a part of the window and any events will return null. It might not be worth trying to get this to look the same across browsers, also because of the mobile implementations, but I happened to be making something like this for no apparent reason. As it coincides with your question I might as well post it.
It's not the prettiest thing when it comes to HTML and CSS because it requires four additional elements - one wrapper (commonly used for styling select boxes with overflow hidden but I took a slightly different approach because I thought it looked better) and three absolutely placed elements. One is a styled button, another will hide the scrollbar that appears and the third is a minor hack.
Most important thing is that the user will not be able to click the select menu itself. When this happens, most is lost because after that it's limbo. For that the third element will be used. It will be put on top of the select box. Then when it's clicked, instead of directly opening the menu it will be faked by changing the size of the select element. The div covering the right side also serves another purpose. It's initially placed at the bottom and by getting it's offset we'll know the height of the box. This will be used to resize the button and set the correct size for the overlaying div.
Looks to be behaving quite predicatbly on all major Windows desktop browsers. For the mobile implications this script uses a touch support feature test and reverts to normal layout if that is the case. Could probably be tweaked (with a screen size check) to not exclude trackpad users.
Demo
Not too much relevant CSS. But important to style body background the same as the bar on the right. Transparency is used so the actual menu isn't visible before the image for the button loads.
$(function() {
var hub = $('#box'), list = $('select'),
impel = $('#open'), commit = $('#mark'), tract = $('#refer'),
zenith = tract.position().top,
extent = list.children().length, active;
if (touch()) {
impel.add(commit).add(tract).remove();
hub.fadeTo(0,1);
return;
}
impel.add(commit).height(zenith);
tract.addClass('bar');
hub.fadeTo(0,1).on('mouseup click', function(e) {
e.stopPropagation();
});
commit.mouseup(function() {
flip();
show();
active = true;
});
list.add(impel).click(function() {
flip();
active = !active;
if (active) show();
else hide();
});
$(window).click(function() {
if (active) {
flip();
hide();
active = false;
}
});
function show() {list.attr('size', extent)}
function hide() {list.removeAttr('size')}
function flip() {commit.toggle()}
function touch() {
return 'ontouchstart' in window
|| navigator.maxTouchPoints > 0
|| navigator.msMaxTouchPoints > 0;
}
});
I have the following function that checks the scrolling position of a user so that the menu becomes fixed once they scroll past the masthead
function checkScrolling() {
if( $(window).scrollTop() > $('.masthead').height() ) { // we check against ACTUAL height
$('.menu').addClass('fixed');
}else {
$('.menu').removeClass('fixed');
}
}
and here the desktop and touch screen event listeners:
$(document).bind('touchstart touchend touchcancel touchleave touchmove', function(e){
checkScrolling();
});
$(window).scroll(function(){
checkScrolling();
});
However the touch events only make the menu fixed during the touchmove reliably. If I scroll really fast with a swipe up or down, there is a delay before the menu becomes fixed or unfixed.
Any ideas on how to fix this? See a code example here: http://dev.driz.co.uk/mobileMasthead.html (has been amended based on some answers below, but still does not work correctly on an iPad or iPhone)
Update:
And reading up on the topic reveals that JS like checking scroll position don't happen during the scroll... BUT... I've noticed that http://www.facebook.com/home/ doesn't have this issue when testing it on my iPad. So it's definitely possible to achieve this effect without using any custom JavaScript scrollbars like iScroll or similar.
Maybe I understand the question wrong, but to ensure functionality with high speed scrolling, why don't you tackle it the pure CSS way (aka faking the 'fancy' effect):
Old School (fast but primitive)
HTML
<div id="menu2"></div>
<div class="scroll" id="scroller">
<div id="overlay"></div>
<div id="menu"></div>
</div>
With the simple CSS:
html, body { overflow: hidden; height: 100% }
body { margin:0; padding:0; }
.scroll {
-webkit-overflow-scrolling: touch;
height:960px;
width:640px;
}
#menu2 {
width:640px;
height:20px;
background:red;
position:absolute;
z-index:-1;
}
#menu {
width:100%;
height:20px;
background:red;
z-index:0;
}
You can see a working example HERE.
It may be a bit primitive: but hey! It works! :)
New School (fancy but slow)
Check all of the other answers supplied.
But note that it 'is known' that the usage of JavaScript in combination with scrolling on mobile devices is causing a lot of trouble regarding speed.
That's why I think the simple CSS approach may be better.
If you want to learn about JavaScript and scrolling on mobile devices (and other functions), then there are two articles which I highly recommend reading:
Fast animations with iOS Webkit
Native scrolling: Grins and Gotchas
Facebook doesn't use JavaScript but pure css:
position: -webkit-sticky;
If i remember it correctly this makes the element stick at the top of its parent container when scrolled.
You just needed to attach your scroll events, not to window, document or body, but to a custom container.
On iOS you can't programatically react during these hardware-accelerated window scrolling behaviour.
Here's a fiddle:
a wrapper:
<div id="content">
some not-so-relevant css:
html,body,#content {
width:100%;
height:100%;
}
#content {
background-color: lightblue;
overflow:scroll;
}
attaching the listeners to the container:
function checkScrolling() {
if ($('#content').scrollTop() > mastheadHeight) {
menu.addClass('fixed');
} else {
menu.removeClass('fixed');
}
}
$('#content').scroll(function () {
checkScrolling();
});
You can see it working here for the JS-only fallback:
http://jsfiddle.net/jaibuu/YqPzS/
direct URL: http://fiddle.jshell.net/jaibuu/YqPzS/show/
I once tried to implement sticky headers on mobile version of a site but had encountered whole set of problems.
First and most important is that scroll event does not fire on mobile devices as often as it does on desktop. Usually it does fire when page stops. I don't know the exact reason for that but I can suggest it's because browsers on mobile "send" such tasks to GPU meantime CPU can not keep JS objects up to date with what happens to the canvas (but that's just my suggestion). Newer version of iOSes are making it better for us and probably scroll will work on iPhones.
You should use touch events. This makes you write a separate version of code for devices that support touch input. So have to look for reliable ways of testing for platform.
But touch events are also tricky especially on Android. I thought that in the callback for touchmove event I will be able to figure out current coordinates and go further from that point.
But There this issue https://code.google.com/p/android/issues/detail?id=5491 on Android, in summary touchmove fires once or twice at the very beginning of the touch action and does not fire again. Which makes it totally useless. People are suggesting preventDefault() but you no longer can scroll with that.
So I ended up with idea to reimplement scrolling from scratch using CSS transforms. And here is my results so far:
http://jsbin.com/elaker/23
Open that link on your device and http://jsbin.com/elaker/23/edit on your desktop: you'll be able to edit code and it will live update on you device, very convenient.
NOTE:
I'd like to warn you that this CSS scrolling thing is raw and there are some known problems that are not resolved yet: like you can sometimes scroll beyond top or bottom boundaries or if you just touch (not move) it still will scroll. Also the notorious URL bar will not hide. So there is work to do.
Do you need the touch events to fire this at all? Modern devices should return $(window).scroll() events and scrollTop values. On older Android and and pre-ios5 (I think), position:fixed: didn't work as expected because the of how the viewport worked. But it has been corrected in all new releases.
To further debug devices, I highly recommend Adobe Edge Inspect. You could console.log the scrollTop and see if the devices you care about actually work correctly with out any trickery. You'll get everything you need with their free version.
A great way to dealing with scroll events is not to attach your checks to the scroll event
takes a lot of resources and doesn't work very well with older browsers.
fortunately you can have a lot more control if you just perform a time loop to do that.
codewise that looks like that:
(It's used by twitter)
var MyMenu = $('#menu'),
didScroll = false;
$(window).scroll(function() {
didScroll = true;
});
setInterval(function() {
if ( didScroll ) {
didScroll = false;
//Do your check here
checkScrolling();
}
}, 180);
You should put your $('.masthead').height() outside this checkScrolling function of yours (in a higher scope) as this kind of operations takes a lot of resources and always ask jquery to "select your element" and calculate its size will eventually make your application laggy:
var headerHeight = $('.masthead').height()
function checkScrolling()
.....
Last thing , you can change the value of the interval attribute (right now it's 180 (a bit more that 60hz -. refresh time of the screen) you can make this value bigger, if you want your app to be more performant but a bit less accurate)
Thanks John Resig and twitter:
http://ejohn.org/blog/learning-from-twitter/
Hope that helped
Ajios !
I have just tested your functions for efficiency. You should try this
function checkScrolling() {
if( $(window).scrollTop() > mastheadHeight )menu.css('position','fixed');
else menu.css('position','');
}
This will reduce function call of addClass and RemoveClass and your execution time will take effect. If you want to reduce more execution time, then better to use pure JavaScript
$(document).ready(function(){
var p = $("#stop").offset().top;
$(window).scroll(function(){
if(p<$(window).scrollTop()){
console.log("div reached");
$("#stop").css({position:"fixed",top:0});
}
else{
console.log("div out");
$("#stop").css({position:"static"});
}
})
});
I think this will help you.
The total code is here in jsfiddle.net.
I have tested it for ipad using safari of online ipad2 simulator in http://alexw.me/ipad2/ and it has worked there. So, I think it will work on real ipad too.
setScrollTop: function(inTop) {
var top = Math.max(0,inTop);
if (wm.isMobile) { // should always be true for touch events
top = Math.min(top, this.listNode.clientHeight - this.listNodeWrapper.clientHeight);
if (dojo.isWebKit) {
this.listNode.style.WebkitTransform = "translate(0,-" + top + "px)";
} else if (dojo.isMoz) {
this.listNode.style.MozTransform = "translate(0,-" + top + "px)";
} else if (dojo.isOpera) {
this.listNode.style.OTransform = "translate(0,-" + top + "px)";
} else if (dojo.isIE) {
this.listNode.style.MsTransform = "translate(0,-" + top + "px)";
}
this.listNode.style.transform = "translate(0,-" + top + "px)";
this._scrollTop = top;
this._onScroll();
} else {
this.listNode.scrollTop = top + "px";
}
},
In my web application I try to implement some drag and drop functionality. I have a global JavaScript component which does the the basic stuff. This object is also responsible for changing the mouse cursor, depending of the current drag operation (move, copy, link). On my web page there are various HTML elements which define an own cursor style, either inline or via a CSS file.
So, is there a way for my central drag and drop component to change the mouse cursor globally, independent from the style of the element the mouse cursor is over?
I tried:
document.body.style.cursor = "move"
and
document.body.style.cursor = "move !important"
But it doesn't work. Every time I drag over an element which defines a cursor style, the cursor changes to that style.
Sure, I could change the style of the element I'm currently dragging over, but then I have to reset it when I leave the element. This seems a little bit to complicated. I'm looking for a global solution.
Important Update (2021):
The MDN page for element.setCapture() clearly indicates that this feature is deprecated and non-standard, and should not be used in production.
The browser support table at the bottom of that page indicates that it's only supported in Firefox and IE.
Original answer below
Please: don't massacre your CSS!
To implement a drag and drop functionality, you have to use a very important API: element.setCapture(), which does the following :
All mouse events are redirected to the target element of the capture, regardless of where they occured (even outside the browser window)
The cursor will be the cursor of the target element of the capture, regardless where the mouse pointer is.
You have to call element.releaseCapture() or document.releaseCapture() to switch back to normal mode at the end of the operation.
Beware of a naïve implementation of drag and drop: you can have a lot of painful issues, like for example (among others): what happens if the mouse is released outside the browser's window, or over an element which has a handler that stops propagation. Using setCapture() solves all this issues, and the cursor style as well.
You can read this excellent tutorial that explains this in detail if you want to implement the drag and drop yourself.
Maybe you could also use jQuery UI draggable if possible in your context.
I tried using setPointerCapture which worked great. The downside is, that (of cause) all pointer events will not work as before. So I lost hover styles etc.
My solution now is pretty straight forward and for my usecase better suited then the above CSS solutions.
To set the cursor, I add a new stylesheet to head:
const cursorStyle = document.createElement('style');
cursorStyle.innerHTML = '*{cursor: grabbing!important;}';
cursorStyle.id = 'cursor-style';
document.head.appendChild(cursorStyle);
To reset it, I simply remove the stylesheet:
document.getElementById('cursor-style').remove();
document.body.style.cursor = "move"
should work just fine.
However, I recommend to do the global styling via CSS.
define the following:
body{
cursor:move;
}
The problem is, that the defined cursors on the other elements override the body style.
You could do someting like this:
your-element.style.cursor = "inherit"; // (or "default")
to reset it to the inherited style from the body or with CSS:
body *{
cursor:inherit;
}
Note however, that * is normally considered a bad selector-choice.
Unfortunately element.setCapture() does not work for IE
I use a brute force approach - open a transparent div on top of entire page for the duration of drag-drop.
.tbFiller {
position:absolute;
z-index:5000;
left:0;
top:0;
width:100%;
height:100%;
background-color:transparent;
cursor:move;
}
...
function dragStart(event) {
// other code...
document.tbFiller=document.createElement("div");
document.tbFiller.className="tbFiller"
}
function dragStop(event) {
// other code...
document.tbFiller.parentNode.removeChild(document.tbFiller);
}
Thanks to some of the other answers here for clues, this works well:
/* Disables all cursor overrides when body has this class. */
body.inheritCursors * {
cursor: inherit !important;
}
Note: I didn't need to use <html> and document.documentElement; instead, <body> and document.body work just fine:
document.body.classList.add('inheritCursors');
This causes all descendant elements of <body> (since it now has this inheritCursors class) to inherit their cursor from <body> itself, which is whatever you set it to:
document.body.style.cursor = 'progress';
Then to yield back control to the descendant elements, simply remove the class:
document.body.classList.remove('inheritCursors');
and to unset the cursor on the <body> to the default do:
document.body.style.cursor = 'unset';
This is what I do and it works in Firefox, Chrome, Edge and IE as of 2017.
Add this CSS rule to your page:
html.reset-all-cursors *
{
cursor: inherit !important;
}
When the <html> element has the "reset-all-cursors" class, this overrides all cursors that are set for elements individually in their style attribute – without actually manipulating the elements themselves. No need to clean up all over the place.
Then when you want to override your cursor on the entire page with that of any element, e. g. the element being dragged, do this in JavaScript:
element.setCapture && element.setCapture();
$("html").addClass("reset-all-cursors");
document.documentElement.style.setProperty("cursor", $(element).css("cursor"), "important");
It uses the setCapture function where it is available. This is currently just Firefox although they say it's a Microsoft API. Then the special class is added to the entire document, which disables all custom cursors. Finally set the cursor you want on the document so it should appear everywhere.
In combination with capturing events, this may even extend the dragging cursor to outside of the page and the browser window. setCapture does this reliably in Firefox. But elsewhere it doesn't work every time, depending on the browser, its UI layout, and the path along which the mouse cursor leaves the window. ;-)
When you're finished, clean up:
element.releaseCapture && element.releaseCapture();
$("html").removeClass("reset-all-cursors");
document.documentElement.style.setProperty("cursor", "");
This includes jQuery for addClass and removeClass. In simple scenarios you could just plain compare and set the class attribute of document.documentElement. This will break other libraries like Modernizr though. You can get rid of the css function if you know the element's desired cursor already, or try something like element.style.cursor.
A performance-acceptable, but not ideal either, solution that I ended up using, is actually to change the cursor prop of element directly under the pointer, and then return it back to original when pointer moved to another element. It works comparatively fast, as just a few elements change their style while moving pointer around, but visually you might sometimes see a short glimpse of "original" cursor. I consider it a much more acceptable tradeoff.
So, the solution, in TypeScript:
let prevElement: HTMLElement | undefined;
let prevElementOriginalCursor: string | undefined;
export const setTemporaryCursor = (element: HTMLElement, cursor: string | undefined) => {
// First, process the incoming element ASAP
let elementOriginalCursor: string | undefined;
requestAnimationFrame(() => {
if (element && prevElement !== element) {
elementOriginalCursor = element.style.cursor;
element.style.cursor = cursor ?? '';
}
});
// Then process the previous element, not so critical
requestAnimationFrame(() => {
if (prevElement && prevElement !== element) {
prevElement.style.cursor = prevElementOriginalCursor ?? '';
prevElementOriginalCursor = elementOriginalCursor;
}
prevElement = element;
});
};
export const resetTemporaryCursor = () => {
if (prevElement) {
prevElement.style.cursor = prevElementOriginalCursor ?? '';
prevElementOriginalCursor = undefined;
prevElement = undefined;
}
};
just call setTemporaryCursor while user moves mouse, and call resetTemporaryCursor() when drag process is wrapped up (on MouseUp for instance).
This does the job for me. The use of requestAnimationFrame is optional, and probably could be improved with experimentation.
I can't seem to capture the scroll event on an iPad.
None of these work, what I am doing wrong?
window.onscroll=myFunction;
document.onscroll=myFunction;
window.attachEvent("scroll",myFunction,false);
document.attachEvent("scroll",myFunction,false);
They all work even on Safari 3 on Windows.
Ironically, EVERY browser on the PC supports window.onload= if you don't mind clobbering existing events. But no go on iPad.
The iPhoneOS does capture onscroll events, except not the way you may expect.
One-finger panning doesn’t generate any events until the user stops panning—an onscroll event is generated when the page stops moving and redraws—as shown in Figure 6-1.
Similarly, scroll with 2 fingers fires onscroll only after you've stopped scrolling.
The usual way of installing the handler works e.g.
window.addEventListener('scroll', function() { alert("Scrolled"); });
// or
$(window).scroll(function() { alert("Scrolled"); });
// or
window.onscroll = function() { alert("Scrolled"); };
// etc
(See also https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
For iOS you need to use the touchmove event as well as the scroll event like this:
document.addEventListener("touchmove", ScrollStart, false);
document.addEventListener("scroll", Scroll, false);
function ScrollStart() {
//start of scroll event for iOS
}
function Scroll() {
//end of scroll event for iOS
//and
//start/end of scroll event for other browsers
}
Sorry for adding another answer to an old post but I usually get a scroll event very well by using this code (it works at least on 6.1)
element.addEventListener('scroll', function() {
console.log(this.scrollTop);
});
// This is the magic, this gives me "live" scroll events
element.addEventListener('gesturechange', function() {});
And that works for me. Only thing it doesn't do is give a scroll event for the deceleration of the scroll (Once the deceleration is complete you get a final scroll event, do as you will with it.) but if you disable inertia with css by doing this
-webkit-overflow-scrolling: none;
You don't get inertia on your elements, for the body though you might have to do the classic
document.addEventListener('touchmove', function(e) {e.preventDefault();}, true);
I was able to get a great solution to this problem with iScroll, with the feel of momentum scrolling and everything https://github.com/cubiq/iscroll The github doc is great, and I mostly followed it. Here's the details of my implementation.
HTML:
I wrapped the scrollable area of my content in some divs that iScroll can use:
<div id="wrapper">
<div id="scroller">
... my scrollable content
</div>
</div>
CSS:
I used the Modernizr class for "touch" to target my style changes only to touch devices (because I only instantiated iScroll on touch).
.touch #wrapper {
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
}
.touch #scroller {
position: absolute;
z-index: 1;
width: 100%;
}
JS:
I included iscroll-probe.js from the iScroll download, and then initialized the scroller as below, where updatePosition is my function that reacts to the new scroll position.
# coffeescript
if Modernizr.touch
myScroller = new IScroll('#wrapper', probeType: 3)
myScroller.on 'scroll', updatePosition
myScroller.on 'scrollEnd', updatePosition
You have to use myScroller to get the current position now, instead of looking at the scroll offset. Here is a function taken from http://markdalgleish.com/presentations/embracingtouch/ (a super helpful article, but a little out of date now)
function getScroll(elem, iscroll) {
var x, y;
if (Modernizr.touch && iscroll) {
x = iscroll.x * -1;
y = iscroll.y * -1;
} else {
x = elem.scrollTop;
y = elem.scrollLeft;
}
return {x: x, y: y};
}
The only other gotcha was occasionally I would lose part of my page that I was trying to scroll to, and it would refuse to scroll. I had to add in some calls to myScroller.refresh() whenever I changed the contents of the #wrapper, and that solved the problem.
EDIT: Another gotcha was that iScroll eats all the "click" events. I turned on the option to have iScroll emit a "tap" event and handled those instead of "click" events. Thankfully I didn't need much clicking in the scroll area, so this wasn't a big deal.
Since iOS 8 came out, this problem does not exist any more. The scroll event is now fired smoothly in iOS Safari as well.
So, if you register the scroll event handler and check window.pageYOffset inside that event handler, everything works just fine.
After some testing on the ios, I found that this is the way to go for ios and desktop, if you are not worried of that delay of 120ms on desktop. Works like a charm.
let isScrolling;
document.addEventListener("scroll", () => {
// Clear our timeout throughout the scroll
window.clearTimeout( isScrolling );
// Set a timeout to run after scrolling ends
isScrolling = setTimeout(function() {
// Run the callback
console.log( 'Scrolling has stopped.' );
}, 120);
});
I want to hide the cursor when showing a webpage that is meant to display information in a building hall. It doesn't have to be interactive at all. I tried changing the cursor property and using a transparent cursor image but it didn't solve my problem.
Does anybody know if this can be done? I suppose this can be thought of as a security threat for a user that can't know what he is clicking on, so I'm not very optimistic... Thank you!
With CSS:
selector { cursor: none; }
An example:
<div class="nocursor">
Some stuff
</div>
<style type="text/css">
.nocursor { cursor:none; }
</style>
To set this on an element in Javascript, you can use the style property:
<div id="nocursor"><!-- some stuff --></div>
<script type="text/javascript">
document.getElementById('nocursor').style.cursor = 'none';
</script>
If you want to set this on the whole body:
<script type="text/javascript">
document.body.style.cursor = 'none';
</script>
Make sure you really want to hide the cursor, though. It can really annoy people.
Pointer Lock API
While the cursor: none CSS solution is definitely a solid and easy workaround, if your actual goal is to remove the default cursor while your web application is being used, or implement your own interpretation of raw mouse movement (for FPS games, for example), you might want to consider using the Pointer Lock API instead.
You can use requestPointerLock on an element to remove the cursor, and redirect all mousemove events to that element (which you may or may not handle):
document.body.requestPointerLock();
To release the lock, you can use exitPointerLock:
document.exitPointerLock();
Additional notes
No cursor, for real
This is a very powerful API call. It not only renders your cursor invisible, but it actually removes your operating system's native cursor. You won't be able to select text, or do anything with your mouse (except listening to some mouse events in your code) until the pointer lock is released (either by using exitPointerLock or pressing ESC in some browsers).
That is, you cannot leave the window with your cursor for it to show again, as there is no cursor.
Restrictions
As mentioned above, this is a very powerful API call, and is thus only allowed to be made in response to some direct user-interaction on the web, such as a click; for example:
document.addEventListener("click", function () {
document.body.requestPointerLock();
});
Also, requestPointerLock won't work from a sandboxed iframe unless the allow-pointer-lock permission is set.
User-notifications
Some browsers will prompt the user for a confirmation before the lock is engaged, some will simply display a message. This means pointer lock might not activate right away after the call. However, the actual activation of pointer locking can be listened to by listening to the pointerchange event on the element on which requestPointerLock was called:
document.body.addEventListener("pointerlockchange", function () {
if (document.pointerLockElement === document.body) {
// Pointer is now locked to <body>.
}
});
Most browsers will only display the message once, but Firefox will occasionally spam the message on every single call. AFAIK, this can only be worked around by user-settings, see Disable pointer-lock notification in Firefox.
Listening to raw mouse movement
The Pointer Lock API not only removes the mouse, but instead redirects raw mouse movement data to the element requestPointerLock was called on. This can be listened to simply by using the mousemove event, then accessing the movementX and movementY properties on the event object:
document.body.addEventListener("mousemove", function (e) {
console.log("Moved by " + e.movementX + ", " + e.movementY);
});
If you want to hide the cursor in the entire webpage, using body will not work unless it covers the entire visible page, which is not always the case. To make sure the cursor is hidden everywhere in the page, use:
document.documentElement.style.cursor = 'none';
To reenable it:
document.documentElement.style.cursor = 'auto';
The analogue with static CSS notation is html {cursor:none} (or, depending on what exactly you want * {cursor:none} / :root {cursor:none}).
I did it with transparent *.cur 1px to 1px, but it looks like small dot. :( I think it's the best cross-browser thing that I can do.
CSS2.1 has no value 'none' for 'cursor' property - it was added in CSS3. Thats why it's workable not everywhere.
If you want to do it in CSS:
#ID { cursor: none !important; }
For whole html document try this
html * {cursor:none}
Or if some css overwrite your cursor: none use !important
html * {cursor:none!important}