DEV Community

Cover image for Let's create a simple React hook to detect browsers and their capabilities
Dima Vyshniakov
Dima Vyshniakov

Posted on

Let's create a simple React hook to detect browsers and their capabilities

User agent sniffing is the most popular approach for browser detection. Unfortunately, it's not very accessible for a front end development because of multiple reasons. Browser vendors constantly trying to make sniffing not possible. Thus, each browser has their own user agent string format, which is very complex to parse.

There is a much more simple way of achieving the same using browser CSS API, which I'm going to show you. So let's create browser capabilities detection React hook.

We are going to use CSS.supports() static method. It returns a boolean value indicating if the browser supports a given CSS feature, or not. This is javascript analog of @supports at-rule. It works similar to media queries, but with CSS capabilities as a subject.

Hook to detect supported features

The most naive approach of calling CSS.supports() during component render cycle will create problems in Server Side Rendering environments, such as Next.js. Because the server side renderer has no access to browser APIs, it just produces a string of code.

import type {FC} from 'react';

const Component: FC = () => {
    // 🚫 Don't do this!
    const hasFeature = CSS.supports('your-css-declaration');
    // ...
}
Enter fullscreen mode Exit fullscreen mode

We will use this simple hook instead. The hook receives a string containing support condition, a CSS rule we are going to validate, e.g. display: flex.

import {useState, useEffect} from 'react';

export const useSupports = (supportCondition: string) => {
    // Create a state to store declaration check result
    const [checkResult, setCheckResult] = useState<boolean | undefined>();

    useEffect(() => {
        // Run check as a side effect, on user side only
        setCheckResult(CSS.supports(supportCondition));
    }, [supportCondition]);


    return checkResult;
};
Enter fullscreen mode Exit fullscreen mode

Now we can check for different CSS features support from inside React component.

import type {FC} from 'react';

const Component: FC = () => {

    // Check for native `transform-style: preserve` support
    const hasNativeTransformSupport = useSupports('
        (transform-style: preserve)
    ');

    // Check for vendor prefixed `transform-style: preserve` support
    const hasNativeTransformSupport = useSupports('
        (-moz-transform-style: preserve) or (-webkit-transform-style: preserve)
    ');
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Detect user browser using CSS support conditions

In order to detect user browser, we have to do a little hacking.

Browser hack has nothing to do with law violations. It's just a special CSS declaration or selector which works differently in one of available browsers.

Here is the reference page with various browser hacks. After thorough experimentation on my machine, I've chosen these:

const hacksMapping = {
    // anything -moz will work, I assume
    firefox: '-moz-appearance:none',
    safari: '-webkit-hyphens:none',
    // tough one because Webkit and Blink are relatives
    chrome: '
        not (-webkit-hyphens:none)) and (not (-moz-appearance:none)) and (list-style-type:"*"'
}
Enter fullscreen mode Exit fullscreen mode

And here is our final hook look like:

export const useDetectBrowser = () => {
    const isFirefox = useSupports(hacksMapping.firefox);
    const isChrome = useSupports(hacksMapping.chrome);
    const isSafari = useSupports(hacksMapping.safari);

    return [
        {browser: 'firefox', condition: isFirefox},
        {browser: 'chromium based', condition: isChrome},
        {browser: 'safari', condition: isSafari},
    ].find(({condition}) => condition)?.browser as 
        'firefox' | 'chromium based' | 'safari' | undefined;
};
Enter fullscreen mode Exit fullscreen mode

Full demo

Here is a full working demo of the hook.

Final thoughts

While I cannot assert that this method is entirely foolproof or stable, it is important to note that browsers frequently undergo updates, and vendor-specific properties are often deprecated or replaced by standardized ones. This issue is equally applicable to user agent sniffing, as both approaches encounter similar challenges.

However, the CSS.supports() method offers a more maintainable and granular solution. It encourages developers to adopt strategies such as graceful degradation or progressive enhancement, allowing for more precise and adaptable patch applications.

Happy coding.

Top comments (4)

Collapse
 
brense profile image
Rense Bakker

Computing state is not a side effect. Use the useMemo hook instead:

const checkResult = useMemo(() => CSS.supports(supportCondition), [supportCondition])
Enter fullscreen mode Exit fullscreen mode

And since it produces a boolean based on a string there's no risk of breaking references here so you should just do this without built in hooks:

function useSupports(supportCondition){
  return CSS.supports(supportCondition)
}
Enter fullscreen mode Exit fullscreen mode

And you could seriously wonder how useful it is to wrap that in a custom hook, instead of just using CSS.supports directly...

Collapse
 
morewings profile image
Dima Vyshniakov • Edited

Thanks for the input.

Why do you think useMemo is better than useState? You snippet will not work with Next.js and other Server side generators for the reason I copied below.

Regarding CSS.supports(). Did you consider this?

The most naive approach of calling CSS.supports() during component render cycle will create problems in Server Side Rendering environments, such as Next.js. Because the server side renderer has no access to browser APIs, it just produces a string of code.

Collapse
 
brense profile image
Rense Bakker

Ok fair enough, I'll admit I read over that and skipped to your code examples. But then I'd still prefer not to useState but add a check for typeof window === 'undefined' instead.

Thread Thread
 
morewings profile image
Dima Vyshniakov

Might be the way. I was doing this when started working with Next.js. Now I think useEffect approach is more flexible. But there are a lot of ways, how to skin the cat.