I'm developing a webapp (not a website with pages of interesting text) with a very different interface for touch (your finger hides the screen when you click) and mouse (relies heavily on hover preview). How can I detect that my user has no mouse to present him the right interface? I plan to leave a switch for people with both mouse and touch (like some notebooks).
The touch event capability in the browser doesn't actually mean the user is using a touch device (for example, Modernizr doesn't cut it). The code that correctly answers the question should return false if the device has a mouse, true otherwise. For devices with mouse and touch, it should return false (not touch only)
As a side note, my touch interface might also be suitable for keyboard-only devices, so it's more the lack of mouse I'm looking to detect.
To make the need more clear, here is the API that I'm looking to implement:
// Level 1
// The current answers provide a way to do that.
hasTouch();
// Returns true if a mouse is expected.
// Note: as explained by the OP, this is not !hasTouch()
// I don't think we have this in the answers already, that why I offer a bounty
hasMouse();
// Level 2 (I don't think it's possible, but maybe I'm wrong, so why not asking)
// callback is called when the result of "hasTouch()" changes.
listenHasTouchChanges(callback);
// callback is called when the result of "hasMouse()" changes.
listenHasMouseChanges(callback);
As of 2018 there is a good and reliable way to detect if a browser has a mouse (or similar input device): CSS4 media interaction features which are now supported by almost any modern browser (except IE 11 and special mobile browsers).
W3C:
The pointer media feature is used to query the presence and accuracy of a pointing device such as a mouse.
See the following options:
/* The primary input mechanism of the device includes a
pointing device of limited accuracy. */
@media (pointer: coarse) { ... }
/* The primary input mechanism of the device
includes an accurate pointing device. */
@media (pointer: fine) { ... }
/* The primary input mechanism of the
device does not include a pointing device. */
@media (pointer: none) { ... }
/* Primary input mechanism system can
hover over elements with ease */
@media (hover: hover) { ... }
/* Primary input mechanism cannot hover
at all or cannot conveniently hover
(e.g., many mobile devices emulate hovering
when the user performs an inconvenient long tap),
or there is no primary pointing input mechanism */
@media (hover: none) { ... }
/* One or more available input mechanism(s)
can hover over elements with ease */
@media (any-hover: hover) { ... }
/* One or more available input mechanism(s) cannot
hover (or there are no pointing input mechanisms) */
@media (any-hover: none) { ... }
Media queries can also be used in JS:
if(window.matchMedia("(any-hover: none)").matches) {
// do something
}
Related:
W3 documentation: https://www.w3.org/TR/mediaqueries-4/#mf-interaction
Browser support: https://caniuse.com/#search=media%20features
Similar problem: Detect if a client device supports :hover and :focus states
The main trouble is that you have the following different classes of devices/use cases:
Mouse and keyboard (desktop) Touch only (phone/tablet) Mouse, keyboard, and touch (touch laptops) Touch and keyboard (bluetooth keyboard on tablet) Mouse only (Disabled user/browsing preference) Keyboard only (Disabled user/browsing preference) Touch and mouse (ie hover events from Galaxy Note 2 pen)
What's worse, is that one can transition from some of these classes to others (plugs in a mouse, connects to keyboard), or a user may APPEAR to be on a normal laptop until they reach out and touch the screen.
You are correct in assuming that the presence of event constructors in the browser is not a good way to move forward (and it is somewhat inconsistent). Additionally, unless you are tracking a very specific event or only trying to rule out a few classes above, using events themselves isn't full proof.
For example, say you've discovered that a user has emitted a real mousemove (not the false one from touch events, see http://www.html5rocks.com/en/mobile/touchandmouse/).
Then what?
You enable hover styles? You add more buttons?
Either way you are increasing time to glass because you have to wait for an event to fire.
But then what happens when your noble user decides wants to unplug his mouse and go full touch.. do you wait for him to touch your now crammed interface, then change it right after he's made the effort to pinpoint your now crowded UI?
In bullet form, quoting stucox at https://github.com/Modernizr/Modernizr/issues/869#issuecomment-15264101
We want to detect the presence of a mouse We probably can't detect it before an event is fired As such, what we're detecting is if a mouse has been used in this session — it won't be immediately from page load We probably also can't detect that there isn't a mouse — it'd be undefined until true (I think this makes more sense than setting it false until proven) And we probably can't detect if a mouse is disconnected mid-session — that'll be indistinguishable from the user just giving up with their mouse
An aside: the browser DOES know when a user plugs in a mouse/connects to a keyboard, but doesn't expose it to JavaScript.. dang!
This should lead you to the following:
Tracking the current capabilities of a given user is complex, unreliable, and of dubious merit
The idea of progressive enhancement applies quite well here, though. Build an experience that works smoothly no matter the context of the user. Then make assumptions based on browser features/media queries to add functionality that will be relative in the assumed context. Presence of a mouse is just one of the multitudes of ways in which different users on different devices experience your website. Create something with merit at its kernel and don't worry too much about how people click the buttons.
:hover
elements and things like that) when user switch from mouse to touch. It seems unlikely that the user is currently using mouse + touch at the exact same time (I mean comoonn it's like having 2 mouses connected to the same computer hahaha)
How about listening for a mousemove event on the document. Then until you hear that event you assume that the device is touch or keyboard only.
var mouseDetected = false;
function onMouseMove(e) {
unlisten('mousemove', onMouseMove, false);
mouseDetected = true;
// initializeMouseBehavior();
}
listen('mousemove', onMouseMove, false);
(Where listen
and unlisten
delegate to addEventListener
or attachEvent
as appropriate.)
Hopefully this wouldn't lead to too much visual jank, it would suck if you need massive re-layouts based on mode...
@Wyatt's answer is great and gives us a lot to think about.
On my case, I chose to listen for the first interaction, to only then set a behavior. So, even if the user has a mouse, I will treat as touch device if first interaction was a touch.
Considering the given order in which events are processed:
touchstart touchmove touchend mouseover mousemove mousedown mouseup click
We can assume that if mouse event gets triggered before touch, it is a real mouse event, not an emulated one. Example (using jQuery):
$(document).ready(function() {
var $body = $('body');
var detectMouse = function(e){
if (e.type === 'mousedown') {
alert('Mouse interaction!');
}
else if (e.type === 'touchstart') {
alert('Touch interaction!');
}
// remove event bindings, so it only runs once
$body.off('mousedown touchstart', detectMouse);
}
// attach both events to body
$body.on('mousedown touchstart', detectMouse);
});
That worked for me
It's only possible to detect if a browser is touch capable. There is no way to know if it actually has a touch screen or a mouse connected.
One can prioritize the use though by listening to touch event instead of mouse event if touch capability is detected.
To detect touch capability cross-browser:
function hasTouch() {
return (('ontouchstart' in window) || // html5 browsers
(navigator.maxTouchPoints > 0) || // future IE
(navigator.msMaxTouchPoints > 0)); // current IE10
}
Then one can use this to check:
if (!hasTouch()) alert('Sorry, need touch!);
or to choose which event to listen to, either:
var eventName = hasTouch() ? 'touchend' : 'click';
someElement.addEventListener(eventName , handlerFunction, false);
or use separate approaches for touch vs. non-touch:
if (hasTouch() === true) {
someElement.addEventListener('touchend' , touchHandler, false);
} else {
someElement.addEventListener('click' , mouseHandler, false);
}
function touchHandler(e) {
/// stop event somehow
e.stopPropagation();
e.preventDefault();
window.event.cancelBubble = true;
// ...
return false; // :-)
}
function mouseHandler(e) {
// sorry, touch only - or - do something useful and non-restrictive for user
}
For mouse one can only detect if the mouse is being used, not if it exists or not. One can setup a global flag to indicate that mouse was detected by usage (similar to an existing answer, but simplified a bit):
var hasMouse = false;
window.onmousemove = function() {
hasMouse = true;
}
(one cannot include mouseup
or mousedown
as these events can also be triggered by touch)
Browsers restricts access to low-level system APIs which is needed to be able to detect features such as hardware capabilities of the system it's being used on.
There is the possibility to perhaps write a plugin/extension to access these but via JavaScript and DOM such detection is limited for this purpose and one would have to write a plugin specific for the various OS platforms.
So in conclusion: such detection can only be estimated by a "good guess".
When Media Queries Level 4 is available in browsers, we will be able to use the "pointer" and "hover" queries to detect devices with a mouse.
If we really want to communicate that information to Javascript, we could use a CSS query to set specific styles according to the device type, and then use getComputedStyle
in Javascript to read that style, and derive the original device type from it.
But a mouse can be connected or unplugged at any time, and the user may be wanting to switch between touch and mouse. So we may need to detect this change, and offer to change interface or do so automatically.
any-pointer
and any-hover
will let you investigate all applicable device capabilities. Nice to get a peek at how we might solve this problem in the future! :)
Since you're planning to offer a way to switch between the interfaces anyway, would it be feasible to simply ask the user to click a link or a button to "enter" the correct version of the application? Then you could remember their preference for future visits. It's not high-tech, but it's 100% reliable :-)
@SamuelRossille No browsers that I'm aware of expose the existence of (or lack thereof) a mouse, unfortunately.
So, with that being said, we just have to try and do the best we can with our existing option... events. I know it's not exactly what you're looking for... agreed it is currently far from ideal.
We can do our best to figure out whether a user is using a mouse or touch at any given moment. Here is a quick and dirty example using jQuery & Knockout:
//namespace
window.ns = {};
// for starters, we'll briefly assume if touch exists, they are using it - default behavior
ns.usingTouch = ko.observable(Modernizr.touch); //using Modernizr here for brevity. Substitute any touch detection method you desire
// now, let's sort out the mouse
ns.usingMouse = ko.computed(function () {
//touch
if (ns.usingTouch()) {
//first, kill the base mousemove event
//I really wish browsers would stop trying to handle this within touch events in the first place
window.document.body.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
}, true);
//remove mouse class from body
$('body').removeClass("mouse");
//switch off touch listener
$(document).off(".ns-touch");
// switch on a mouse listener
$(document).on('mousemove.ns-mouse', function (e) {
if (Math.abs(window.lastX - e.clientX) > 0 || window.lastY !== e.clientY) {
ns.usingTouch(false); //this will trigger re-evaluation of ns.usingMouse() and result in ns.usingMouse() === true
}
});
return false;
}
//mouse
else {
//add mouse class to body for styling
$('body').addClass("mouse");
//switch off mouse listener
$(document).off(".ns-mouse");
//switch on a touch listener
$(document).on('touchstart.ns-touch', function () { ns.usingTouch(true) });
return true;
}
});
//tests:
//ns.usingMouse()
//$('body').hasClass('mouse');
You can now bind/subscribe to usingMouse() & usingTouch() and/or style your interface with the body.mouse class. The interface will switch back and forth as soon as a mouse cursor is detected and on touchstart.
Hopefully we'll have some better options from the browser vendors soon.
This worked for me in a similar situation. Basically, assume the user doesn't have a mouse until you see a short series of consecutive mousemoves, without intervening mousedowns or mouseups. Not very elegant, but it works.
var mousedown = false;
var mousemovecount = 0;
function onMouseDown(e){
mousemovecount = 0;
mousedown = true;
}
function onMouseUp(e){
mousedown = false;
mousemovecount = 0;
}
function onMouseMove(e) {
if(!mousedown) {
mousemovecount++;
if(mousemovecount > 5){
window.removeEventListener('mousemove', onMouseMove, false);
console.log("mouse moved");
$('body').addClass('has-mouse');
}
} else {
mousemovecount = 0;
}
}
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mouseup', onMouseUp, false);
Why don't you just detect if it has the ability to sense touches and/or to react to mouse movements?
// This will also return false on
// touch-enabled browsers like Chrome
function has_touch() {
return !!('ontouchstart' in window);
}
function has_mouse() {
return !!('onmousemove' in window);
}
Tera-WURFL can tell you the capabilities of the device that is visiting your site by comparing the browser signature against its database. Give it a look, its free!
I ran into the same issue, where a single touch was also registered as a click. After I read through the comments of top voted answers, i came up with my own solution:
var body = document.getElementsByTagName('body')[0]; var mouseCount = 0; // start in an undefined state // (i use this to blend in elements once we decide what input is used) var interactionMode = 'undefined'; var registerMouse = function() { // count up mouseCount every time, the mousemove event is triggered mouseCount++; // but dont set it instantly. // instead wait 20 miliseconds (seems to be a good value for multiple move actions), // if another mousemove event accoures switch to mouse as interaction setTimeout(function() { // a touch event triggers also exactly 1 mouse move event. // So only if mouseCount is higher than 1 we are really moving the cursor by mouse. if (mouseCount > 1) { body.removeEventListener('mousemove', registerMouse); body.removeEventListener('touchend', registerTouch); interactionMode = 'mouse'; console.log('now mousing'); listenTouch(); } // set the counter to zero again mouseCount = 0; }, 20); }; var registerTouch = function() { body.removeEventListener('mousemove', registerMouse); body.removeEventListener('touchend', registerTouch); interactionMode = 'touch'; console.log('now touching'); mouseCount = 0; listenMouse(); }; var listenMouse = function() { body.addEventListener("mousemove", registerMouse); }; var listenTouch = function() { body.addEventListener("touchend", registerTouch); }; listenMouse(); listenTouch(); // after one second without input, assume, that we are touching // could be adjusted to several seconds or deleted // without this, the interactionMode would stay 'undefined' until first mouse or touch event setTimeout(function() { if (!body.classList.contains('mousing') || body.classList.contains('touching')) { registerTouch(); } }, 1000); /* fix, so that scrolling is possible */ html, body { height: 110%; } Mouse or touch me
The only problem i found is, that you have to be able to scroll, to properly detect a touch event. a single tab(touch) might make problems.
As others have pointed out, definitively detecting whether or not they have a mouse is unreliable. This can easily change, depending on the device. It's definitely something you cannot do reliably with a boolean true or false, at least on a document scale.
Touch events and mouse events are exclusive. So this can help somewhat on taking different actions. The problem is the touch events are closer to the mouse up/down/move events, and also trigger a click event.
From you question you say you want to have a hover to preview. Beyond that I don't know any other specifics about your interface. I'm assuming that with the lack of a mouse you want a tap to preview, while a click does a different action because of the hover preview.
If that is the case you can take somewhat of a lazy approach to detection:
An onclick event will always be preceded by an onmouseover event with a mouse. So make a note that the mouse is on top of the element that has been clicked.
You could do this with a document-wide onmousemove event. You can use event.target
to record which element the mouse is residing on. Then inside your onclick events you can check to see whether or not the mouse is actually over the element being clicked (or a child of the element).
From there you can choose either to rely on the click event for both and take an A or B action depending on the result. The B action could be nothing if some touch devices don't emit a click event (instead you would have to rely on ontouch* events).
I don't think it's possible to identify touch-only device (to my knowledge of course). The main issue is all mouse and keyboard events are fired by touch devices too. See the following example, both alerts return true for touch devices.
function is_touch_present() {
return ('ontouchstart' in window) || ('onmsgesturechange' in window);
}
function is_mouse_present() {
return (('onmousedown' in window) && ('onmouseup' in window) && ('onmousemove' in window) && ('onclick' in window) && ('ondblclick' in window) && ('onmousemove' in window) && ('onmouseover' in window) && ('onmouseout' in window) && ('oncontextmenu' in window));
}
alert("Touch Present: " + is_touch_present());
alert("Mouse Present: " + is_mouse_present());
true
for 'onmousedown' in window
The best idea in my opinion is the mousemove
listener (currently the top answer). I believe that this method needs to be tweaked a bit. It is true that touch-based browsers emulate even the mousemove event, as you can see in this iOS discussion, so we should be a little careful.
It makes sense that touch-based browsers will only emulate this event when the user taps the screen (the user's finger is down). This means we should add a test during our mousemove handler to see which mouse button is down (if any) during the event. If no mouse button is down, we can safely assume a real mouse is present. If a mouse button is down, the test remains inconclusive.
So how would this be implemented? This question shows that the most reliable method to examine which mouse button is down during a mousemove is to actually listen for 3 events in document level: mousemove, mousedown and mouseup. The up and down will only set a global boolean flag. The move will perform the test. If you have a move and the boolean is false, we can assume a mouse is present. See question for exact code examples.
One final comment.. This test isn't ideal because it can't be performed in load time. Therefore, I would use a progressive enhancement method as previously suggested. By default show a version which does not support the mouse-specific hover interface. If a mouse is discovered, enable this mode in runtime using JS. This should appear as seamless as possible to the user.
In order to support changes in the user's configuration (ie mouse has been disconnected), you can periodically re-test. Although I believe it will be better in this case to simply notify the user about the 2 modes and let users manually switch between them (much like the mobile/desktop choice which can always be reversed).
Ran some tests on various PC's, Linux's, iPhone, Android phones and tabs. Weird that there is no easy bullet-proof solution. Problem arises when some that have Touch and no mouse still present Touch and Mouse events to application. Since do want to support mouse-only and touch-only instances, want to process both, but this causes double occurrences of user interactions. If can know mouse is not present on device, then can know to ignore fake/inserted mouse events. Tried setting flag if MouseMove is encountered, but some browsers throw fake MouseMove as well as MouseUp and MouseDown. Tried examining timestamps but figured this was too risky. Bottom line: I found the browsers that created the fake mouse events always inserted a single MouseMove just prior to the inserted MouseDown. In 99.99% of my cases, when running on a system that has a real mouse, there are multiple consecutive MouseMove events - at least two. So, keep track of whether system encounters two consecutive MouseMove events and declare there is no mouse present if this condition is never met. This is probably too simple, but its working on all my test setups. Think I'll stick with it until I find a better solution. - Jim W
A simple solution in jQuery to detect mouse usage, which solves the issue where mobile devices also trigger 'mousemove' event. Simply add a touchstart listener to remove the mousemove listener, so that it doesn't get triggered on touch.
$('body').one('touchstart.test', function(e) {
// Remove the mousemove listener for touch devices
$('body').off('mousemove.test');
}).one('mousemove.test', function(e) {
// MOUSE!
});
Of course, the device could still be touch AND mouse, but the above will guarantee that a real mouse was used.
Just found a solution that I think is quite elegant.
// flag as mouse interaction by default
var isMouse = true;
// detect a touch event start and flag it
$(window).on('touchstart', function () {
// if triggers touchstart, toggle flag
// since touch events come before mouse event / click
// callback of mouse event will get in callback
// `isMouse === false`
isMouse = false;
});
// now the code that you want to write
// should also work with `mouse*` events
$('a.someClass').on('click', function () {
if (isMouse) {
// interaction with mouse event
// ...
} else {
// reset for the next event detection
// this is crucial for devices that have both
// touch screen and mouse
isMouse = true;
// interaction with touch event
// ...
}
});
I spent hours figuring out this problem for my Phonegap app and I came up with this hack. It generates a console warning if the event triggered is a "passive" event, meaning it doesn't actuate any change, but it works! I would be interested in any ideas to improve or a better method. But this effectively allows my to use $.touch() universally.
$(document).ready(function(){ $("#aButton").touch(function(origElement, origEvent){ console.log('Original target ID: ' + $(origEvent.target).attr('id')); }); }); $.fn.touch = function (callback) { var touch = false; $(this).on("click", function(e){ if (!touch) { console.log("I click!"); let callbackReal = callback.bind(this); callbackReal(this, e); }else{ touch = true; } touch = false; }); $(this).on("touchstart", function(e){ if (typeof e.touches != typeof undefined) { e.preventDefault(); touch = true; console.log("I touch!"); let callbackReal = callback.bind(this); callbackReal(this, e); } }); }
the main problem I see here is that most touch devices fire a mouse event along with the coresponding touch one (touchstart -> mousedown, touchmove -> mousemove, etc). For the keyboard only ones, at last for the modern ones, they have a generic browser so you can't even detect the presence of the MouseEvent class.
The less painful solution here would be, in my opinion, to display a menu at launch (with 'alt' management for the keyboard only users) and maybe storing the choice with localStorage/cookies/serverside or else to keep the same choice the next time the visitor comes.
Run the snippet (below) to try this out:
var msg = (window.matchMedia("(any-pointer: coarse)").matches ? "Touchscreen" : "Mouse");
var msg = (window.matchMedia("(any-pointer: coarse)").matches ? "Touchscreen" : "Mouse"); document.getElementById('info').innerHTML = msg; body { background: black; color: cornflowerblue; } #info { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-size: 30vmin; font-family: verdana, arial, helvetica; }
(Here's more info about window.matchMedia
.)
window.matchMedia("(any-pointer: coarse)").matches
returns true on mobile devices and false on desktop but it doesn't take into account connected mouse or s-pen.
I'd strongly recommend against this approach. Consider touch-screen, desktop-sized devices, and you have a different set of problems to solve.
Please make your app usable without a mouse (i.e. no hover preview).
It is generally a better idea to detect if the mouseover function is supported rather than detecting the OS/browser type. You can do that simply by the following:
if (element.mouseover) {
//Supports the mouseover event
}
Be sure that you don't do the following:
if (element.mouseover()) {
// Supports the mouseover event
}
The latter would actually call the method, rather than check for its existence.
You can read up more here: http://www.quirksmode.org/js/support.html
Success story sharing
window.matchMedia("(any-pointer: fine)").matches
returns true on all my mobile browsers and desktop for some reason.window.matchMedia("(any-hover: hover)").matches
always returns true too, even on mobile devices without a mouse. Onlywindow.matchMedia("(any-pointer: coarse)").matches
returns true on mobile devices and false on desktop but it doesn't take into account connected mouse or s-pen.