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');
// ...
}
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;
};
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)
');
// ...
}
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:"*"'
}
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;
};
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 (7)
Computing state is not a side effect. Use the useMemo hook instead:
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:
And you could seriously wonder how useful it is to wrap that in a custom hook, instead of just using
CSS.supports
directly...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?
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.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.
Hi @brense, could you kindly provide a full implementation of your approach to this, I'd love to compare. Thank you!
This approach has some problems. useMemo runs only once. And if you use next the result will always be undefined, because react-dom renderer is not a browser. And you can’t run DOM methods in main execution flow for the same reason.
But if you don’t care about SSR, you can do as @brense proposed. Just save CSS.contains result to variable and that’s it.
Thank you, @morewings.