DEV Community

Cover image for How to detect touch devices using browser media queries
Dima Vyshniakov
Dima Vyshniakov

Posted on

How to detect touch devices using browser media queries

Web developers often need to determine the input methods available on a user's device, such as touchscreen, mouse, stylus, or combinations thereof. This knowledge helps us to create user interfaces that are accessible and optimized for the specific devices and user interactions.

While common approaches like screen size checks, browser user-agent sniffing, or triggering touch events might seem straightforward, they frequently lead to inaccurate results. These methods rely on assumptions that can easily be invalidated, such as a user connecting a mouse to their smartphone, rendering size-based assumptions useless.

CSS Media Queries provide a more reliable and elegant solution. They allow us to get precise and correct information based on specific device characteristics, including input modalities. In this article, we will study pointer and hover Media Queries. Furthermore, I will show how to implement them as a React hooks.

pointer media query

The pointer media query checks whether the device has a primary pointer input capability (like a mouse or touch panel) and how precise it is.

This query returns one of these results:

  • none: no pointing device is available (e.g., voice-controlled devices).

  • coarse: a pointing device is present, but it's not very precise (e.g., a finger on a touchscreen).

  • fine: a highly accurate pointing device is available (e.g., a mouse cursor).

window.matchMedia method allows us to conveniently access Media Queries' results from the code.

useMatchMedia hook

To avoid code duplication, we will create the src/useMatchMedia.ts hook to be able to get results of Media Queries and track their changes.

The window.matchMedia method is used to evaluate the media query. If the result of the media query (media.matches) is different from the current state (matches), the state is updated.

An event listener is added to the media query to listen for changes. When the media query's match status changes, the state is updated. The cleanup function at the return block removes the event listener when the component unmounts or when the media query parameter changes.

export const useMatchMedia = (query: string) => {
  const [matches, setMatches] = useState(false);

  useEffect(() => {

    const media = window.matchMedia(query);

    if (media.matches !== matches) {
      setMatches(media.matches);
    }

    const listener = () => setMatches(media.matches);
    media.addEventListener('change', listener);

    return () => media.removeEventListener('change', listener);
  }, [matches, query]);

  return matches;
};
Enter fullscreen mode Exit fullscreen mode

Detect primary pointer capabilities

Primary pointer toggle

Now we can create src/usePrimaryPointerQuery.ts which allows determining the primary pointer type of the user's input device. The useMatchMedia hook is called with three different media queries to check for none, coarse, and fine pointer types. The hook checks the results of the media queries and returns the corresponding value from the Pointers enum.

import { useMatchMedia } from './useMatchMedia.ts';

const enum Pointers {
  none = 'none',
  coarse = 'coarse',
  fine = 'fine',
}

export const usePrimaryPointerQuery = () => {
  const isNone = useMatchMedia('(pointer: none)');
  const isCoarse = useMatchMedia('(pointer: coarse)');
  const isFine = useMatchMedia('(pointer: fine)');
  if (isNone) {
    return Pointers.none;
  } else if (isCoarse) {
    return Pointers.coarse;
  } else if (isFine) {
    return Pointers.fine;
  }
};
Enter fullscreen mode Exit fullscreen mode

Detect additional pointer(s)

There can be only one primary pointer by definition, but a device can also have various secondary pointers, such a Bluetooth keyboards, joysticks, e.t.c.

any-pointer media query checks the precision of any pointer device available.

Here is the src/useAnyPointerQuery.ts hook. It is defined to accept a pointer parameter, which is a key of the Pointers enum. Inside the function, the useMatchMedia hook is called with a media query string that includes the pointer parameter.

import { Pointers } from './types.ts';
import { useMatchMedia } from './useMatchMedia.ts';

export const useAnyPointerQuery = (pointer: keyof typeof Pointers) => {
  return useMatchMedia(`(any-pointer: ${pointer})`);
};
Enter fullscreen mode Exit fullscreen mode

Advanced detection capabilities

hover and any-hover media queries allow us to detect devices with even more precision and confidence.

These queries are used to test whether primary or any available input mechanism can hover over elements. In our case, it allows making assumptions about pointer device even more precisely using the following logic.

Pointer value Hover value Device
coarse none Modern touch screens
fine none Stylus based
coarse hover Joystick or TV remote
fine hover Mouse

We extend src/types.ts with possible hover query results.

// ...
export const enum Hovers {
  none = 'none',
  hover = 'hover'
}
Enter fullscreen mode Exit fullscreen mode

Now we can build create src/useAnyStylusQuery.ts hook capable of detecting devices equipped with a stylus (any mode). We achieve this by combining two media queries.

import { useMatchMedia } from './useMatchMedia.ts';
import { Pointers, Hovers } from './types.ts';

const useAnyStylusQuery = () => {
  return useMatchMedia(
    `(any-pointer: ${Pointers.fine}) and (any-hover: ${Hovers.none})`,
  );
};

Enter fullscreen mode Exit fullscreen mode

Here is how our hooks detect iPad with stylus.

iPad with stylus

Working demo

Here is a full demo of the hook.

Happy coding!

Top comments (0)