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;
};
Detect primary pointer capabilities
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;
}
};
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})`);
};
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'
}
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})`,
);
};
Here is how our hooks detect iPad with stylus.
Working demo
Here is a full demo of the hook.
Happy coding!
Top comments (0)