Here’s a fun one: Ever have some UI/UX interaction that needed to be one thing for mouses and another thing for touch devices? Ever used Modernizr? If you have, you may be familiar with this gem:
Warning: Indicates if the browser supports the Touch Events spec, and does not necessarily reflect a touchscreen device
You might also know that modern browsers sometimes report .has-touchevents
when there’s a trackpad connected to the system. For instance, when you’re on a laptop, even if you don’t have a touchscreen connected. This phenomenon can be observed in some builds of Chrome on some versions of Windows.
The Modernizr detect for touch screen basically just checks to see if the window contains the dom events for touch. So it only detects if the browser supports touch, not if it is actually utilizing touch hardware. To my knowledge, JavaScript running in browsers currently has no way of determining if a touch device is being used.
So what should you do? The best practice answer is: don’t do anything for either that wouldn’t work correctly for both. Unfortunately, this isn’t always the case in real-life, especially when dealing with client requirements or complex UX interactions. Recently I ran into an issue where I had to detect the use of a touchscreen instead of mouse use for a WCAG accessibility update. Long-story-short: The feature I was having to fix wasn’t compatible with on-screen keyboards, so I had to come up with a way to detect touch screen usage the most accurately I could.
Enter Tactus
In attempting to find an existing solution, I stumbled upon the quintessential “has touch events” JavaScript detect.
if ('ontouchstart' in window || navigator.msMaxTouchPoints > 0) {...}
This would only get me part of the way there and is moderately useful, considering this is what Modernizr uses. Instead of only detecting the existence of touch events, I decided that I’d have to listen for them to see if a touch device is actually being used. This is what I came up with:
// Make sure that the `WEB` namespace object exists and the `utils` container exists within it WEB = WEB || {}; WEB.utils = WEB.utils || {}; // Begin Tactus WEB.utils.tactus = (function() { // Set variable to cache the results of isTouch var _isTouchCached = null; // Set a variable detecting the existence of touch events var _hasEvents = 'ontouchstart' in window || navigator.msMaxTouchPoints > 0; // Set a flag variable to keep track of if the mouse/touch event listeners were already added var _listenersAdded = false; // Call the isTouch method initially isTouch(); // Reveal the methods return { isTouch: isTouch, // WEB.utils.tactus.isTouch(); est: isTouch // WEB.utils.tactus.est(); }; // Default isTouch check function isTouch() { // If the cached variable already has a value if (_isTouchCached !== null) { // Just return the cached variable and don't bother checking again return _isTouchCached; } // If the browser supports touch events if (_hasEvents) { // If the mouse/touch event listeners haven't been added yet if (!_listenersAdded) { // Add the mouse/touch event listeners _addListeners(); } // If the browser doesn't support touch events } else { // Set the cached variable to false _isTouchCached = false; } // Return the current value of the cached variable return _isTouchCached; } // The function to add the mouse/touch event listeners function _addListeners() { // If the mouse/touch event listeners haven't been added yet if (!_listenersAdded) { // Add the mouse event listener window.addEventListener('mousemove', _checkForMouse); // Add the touch event listener window.addEventListener('touchstart', _checkForTouch); } // Set the flag to true to indicate that the mosue/touch event listeners have been added _listenersAdded = true; } // The mouse event listener function _checkForMouse() { // Set the cached variable to false _isTouchCached = false; // Remove the mouse/touch event listeners _removeListeners(); } // The touch event listener function _checkForTouch() { // Set the cached variable to true _isTouchCached = true; // Remove the mouse/touch event listeners _removeListeners(); } // The function to remove the mouse/touch event listeners function _removeListeners() { // Remove the mouse event listener window.removeEventListener('mousemove', _checkForMouse); // Remove the touch event listener window.removeEventListener('touchstart', _checkForTouch); } }());
This utility uses the revealing module pattern and a namespaced object and utils
container.
We only want to listen for either the mousemove
event, or the touchstart
event once. Whenever either of them is fired, it sets the variable appropriately and removes the events. Any time the
isTouch()
function is called, it returns the cached variable; once either of the event listeners has fired, it will be set and every subsequent call to isTouch()
will return the same boolean value.
On DOM ready, this function will fire; it will attach the event listeners and begin waiting for the user’s input. Unfortunately, any other checks for isTouch()
that happen on DOM ready (before the user has a chance to move their mouse or touch their screen) will all return null
. I chose null
since it is falsey. Any calls to check isTouch()
that execute before user input will mimic the behavior of the user using the mouse only; think of this as the default. You can then use future calls to isTouch()
as a progressive enhancement for touchscreen devices.
This could be made into a promise or observable paradigm so that the call wouldn’t actually return until the user interacts; though the use of promises is outside the scope of this post.
TL;DR
If you wait for the user’s input, with either a mousemove
, or a touchstart
event, you can more accurately determine if the user is actually using a touch device.
I look forward to seeing examples of this being used or altered to include the use of promises or observables.