If you want to avoid using user agent detection, you have options!
- Feature detection
-
Feature detection is where you don't try to figure out which browser is rendering your page, but instead, you check to see if the specific feature you need is available. If it's not, you use a fallback. In those rare cases where behavior differs between browsers, instead of checking the user agent string, you should instead implement a test to detect how the browser implements the API and determine how to use it from that. An example of feature detection is as follows. In 2017, Chrome unflagged experimental lookbehind support in regular expressions, but no other browser supported it. So, you might have thought to do this:
let splitUpString;
if (navigator.userAgent.includes("Chrome")) {
const camelCaseExpression = new RegExp("(?<=[A-Z])");
splitUpString = (str) => String(str).split(camelCaseExpression);
} else {
splitUpString = (str) => str.replace(/[A-Z]/g, "z$1").split(/z(?=[A-Z])/g);
}
console.log(splitUpString("fooBare"));
console.log(splitUpString("jQWhy"));
The above code would have made several incorrect assumptions: First, it assumed that all user agent strings that include the substring "Chrome" are Chrome. UA strings are notoriously misleading. Then, it assumed that the lookbehind feature would always be available if the browser was Chrome. The agent might be an older version of Chrome, from before support was added, or (because the feature was experimental at the time) it could be a later version of Chrome that removed it. Most importantly, it assumed no other browsers would support the feature. Support could have been added to other browsers at any time, but this code would have continued choosing the inferior path.
Problems like these can be avoided by testing for support of the feature itself instead:
let isLookBehindSupported = false;
try {
new RegExp("(?<=)");
isLookBehindSupported = true;
} catch (err) {
}
const splitUpString = isLookBehindSupported
? (str) => String(str).split(new RegExp("(?<=[A-Z])"))
: (str) => str.replace(/[A-Z]/g, "z$1").split(/z(?=[A-Z])/g);
As the above code demonstrates, there is always a way to test browser support without user agent sniffing. There is never any reason to check the user agent string for this.
Lastly, the above code snippets bring about a critical issue with cross-browser coding that must always be taken into account. Don't unintentionally use the API you are testing for in unsupported browsers. This may sound obvious and simple, but sometimes it is not. For example, in the above code snippets, using lookbehind in short-regexp notation (for example, /reg/igm) will cause a parser error in unsupported browsers. Thus, in the above example, you would use new RegExp("(?<=look_behind_stuff)"); instead of /(?<=look_behind_stuff)/, even in the lookbehind supported section of your code.
- Progressive enhancement
-
This design technique involves developing your Web site in 'layers', using a bottom-up approach, starting with a simpler layer and improving the capabilities of the site in successive layers, each using more features.
- Graceful degradation
-
This is a top-down approach in which you build the best possible site using all the features you want, then tweak it to make it work on older browsers. This can be harder to do, and less effective, than progressive enhancement, but may be useful in some cases.
- Mobile device detection
-
Arguably the most common use and misuse of user agent sniffing is to detect if the device is a mobile device. However, people too often overlook what they are really after. People use user agent sniffing to detect if the users' device is touch-friendly and has a small screen so they can optimize their website accordingly. While user agent sniffing can sometimes detect these, not all devices are the same: some mobile devices have big screen sizes, some desktops have a small touchscreen, some people use smart TV's which are an entirely different ballgame altogether, and some people can dynamically change the width and height of their screen by flipping their tablet on its side! So, user agent sniffing is definitely not the way to go. Thankfully, there are much better alternatives. Use Navigator.maxTouchPoints to detect if the user's device has a touchscreen. Then, default back to checking the user agent screen only if (!("maxTouchPoints" in navigator)) { /*Code here*/}. Using this information of whether the device has a touchscreen, do not change the entire layout of the website just for touch devices: you will only create more work and maintenance for yourself. Rather, add in touch conveniences such as bigger, more easily clickable buttons (you can do this using CSS by increasing the font size). Here is an example of code that increases the padding of #exampleButton to 1em on mobile devices.
let hasTouchScreen = false;
if ("maxTouchPoints" in navigator) {
hasTouchScreen = navigator.maxTouchPoints > 0;
} else if ("msMaxTouchPoints" in navigator) {
hasTouchScreen = navigator.msMaxTouchPoints > 0;
} else {
const mQ = matchMedia?.("(pointer:coarse)");
if (mQ?.media === "(pointer:coarse)") {
hasTouchScreen = !!mQ.matches;
} else if ("orientation" in window) {
hasTouchScreen = true;
} else {
const UA = navigator.userAgent;
hasTouchScreen =
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
}
}
if (hasTouchScreen) {
document.getElementById("exampleButton").style.padding = "1em";
}
As for the screen size, use window.innerWidth and window.addEventListener("resize", () => { /*refresh screen size dependent things*/ }). What you want to do for screen size is not slash off information on smaller screens. That will only annoy people because it will force them to use the desktop version. Rather, try to have fewer columns of information in a longer page on smaller screens while having more columns with a shorter page on larger screen sizes. This effect can be easily achieved using CSS flexboxes, sometimes with floats as a partial fallback.
Also try to move less relevant/important information down to the bottom and group the page's content together meaningfully. Although it is off-topic, perhaps the following detailed example might give you insights and ideas that persuade you to forgo user agent sniffing. Let us imagine a page composed of boxes of information; each box is about a different feline breed or canine breed. Each box has an image, an overview, and a historical fun fact. The pictures are kept to a maximum reasonable size even on large screens. For the purposes of grouping the content meaningfully, all the cat boxes are separated from all the dog boxes such that the cat and dog boxes are not intermixed together. On a large screen, it saves space to have multiple columns to reduce the space wasted to the left and to the right of the pictures. The boxes can be separated into multiple columns via two equally fair method. From this point on, we shall assume that all the dog boxes are at the top of the source code, that all the cat boxes are at the bottom of the source code, and that all these boxes have the same parent element. There a single instance of a dog box immediately above a cat box, of course. The first method uses horizontal Flexboxes to group the content such that when the page is displayed to the end user, all the dogs boxes are at the top of the page and all the cat boxes are lower on the page. The second method uses a Column layout and resents all the dogs to the left and all the cats to the right. Only in this particular scenario, it is appropriate to provide no fallback for the flexboxes/multicolumns, resulting in a single column of very wide boxes on old browsers. Also consider the following. If more people visit the webpage to see the cats, then it might be a good idea to put all the cats higher in the source code than the dogs so that more people can find what they are looking for faster on smaller screens where the content collapses down to one column.
Next, always make your code dynamic. The user can flip their mobile device on its side, changing the width and height of the page. Or, there might be some weird flip-phone-like device thing in the future where flipping it out extends the screen. Do not be the developer having a headache over how to deal with the flip-phone-like device thing. Never be satisfied with your webpage until you can open up the dev tools side panel and resize the screen while the webpage looks smooth, fluid, and dynamically resized. The simplest way to do this is to separate all the code that moves content around based on screen size to a single function that is called when the page is loaded and at each resize event thereafter. If there is a lot calculated by this layout function before it determines the new layout of the page, then consider debouncing the event listener such that it is not called as often. Also note that there is a huge difference between the media queries (max-width: 25em)
, not all and (min-width: 25em)
, and (max-width: 24.99em)
: (max-width: 25em)
excludes (max-width: 25em)
, whereas not all and (min-width: 25em)
includes (max-width: 25em)
. (max-width: 24.99em)
is a poor man's version of not all and (min-width: 25em)
: do not use (max-width: 24.99em)
because the layout might break on very high font sizes on very high definition devices in the future. Always be very deliberate about choosing the right media query and choosing the right >=, <=, >, or < in any corresponding JavaScript because it is very easy to get these mixed up, resulting in the website looking wonky right at the screen size where the layout changes. Thus, thoroughly test the website at the exact widths/heights where layout changes occur to ensure that the layout changes occur properly.