CSS media queries have an equivalent JavaScript function: window.matchMedia
. But what about @supports
feature queries?
Take this code:
const prefersDarkColorSchemeQuery = window.matchMedia(
'(prefers-color-scheme: dark)'
);
const prefersDarkMode = (prefersDarkColorSchemeQuery.matches);
A true
result tells us that a user prefers dark mode. But false
or undefined
is ambiguous. It can either mean that no preference has been set or that the query is not supported by the client browser. The latter is true on Safari 12, the latest, but outdated, web browser that you can officially get on iPhone 7.
Use cases for CSS feature queries in JavaScript
We can easily distinguish between media queries and feature queries in CSS, although it might not make much difference at first sight. We could define a grey default theme and switch to a more opinionated light or dark theme if there is an explicit preference. We can do this without JavaScript:
Dark theme, light theme, default theme 🌝🌚🤔🔦💡
body { /* default theme */
background-color: whitesmoke;
color: darkslategray;
}
@supports (prefers-color-scheme: light) {
body { /* light theme */
background-color: white;
color: black;
}
@media (prefers-color-scheme: dark) {
body { /* dark theme */
background-color: black;
color: white;
}
}
}
Rotten Apple browsers 🍎🍏🤢🤮
What else? I mentioned outdated Safari versions (also known as "rotten Apple" browsers or the "new Internet Explorer"). Some people like to use their devices for a long time to save money and prevent electronic waste. While most Android users can get up-to-date browsers thanks to Firefox and Chrome, Apple prevents browser updates beyond the official end of support.
Feature detection vs. bug(fix) detection
If you search for best practices for browser detection, everyone will tell you not to do it and use feature detection instead. But how do you detect bugs and bug fixes?
Don't do browser detection ...
The creative (ab)use of feature detection to target specific browser bugs is also known as a "browser hack" or "CSS hack" because it relies on undocumented and unrelated features. Sometimes, this is still the most practical way to apply a workaround only to some specific devices without the risk of unpredictable side effects on tested production code.
... do feature detection instead!
I recently discovered an odd behavior on old iPhones that I could reproduce up to Safari 12, but I wasn't able to find any helpful documentation or even a bug report. I decided to disable several progressive enhancements on Safari 12 by treating it like the user had set a preference for reduced motion.
No problem in CSS.
@media (prefers-reduced-motion) {
body .decoration__container {
transform: translateZ(0);
}
}
@supports (not (prefers-color-scheme: dark)) {
body .decoration__container {
transform: translateZ(0);
}
}
But what if we have some enhancements that are initialized in JavaScript?
const prefersReducedMotionQuery = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
const prefersReducedMotion = (prefersReducedMotionQuery.matches);
if (!prefersReducedMotion) {
// initialize some blinking external social media widget
Now we need the JavaScript equivalent of CSS feature queries. There is no "window.matchSupports
", but there is a CSS interface object with a static supports() method.
const supportsReducedMotion = CSS.supports(
'prefers-reduced-motion: reduce'
);
To target my Safari bug workaround, I will detect browser support for color scheme preference, which I found in Apple's release notes for Safari 13.
const supportsColorScheme = CSS.supports(
'prefers-color-scheme: dark'
);
CSS feature detection in JavaScript
CSS.supports
is only available from Safari 11. If we need to include older browsers, we can check if certain objects and properties are present in the document.body.style
object, as described by Lea Verou in her 2009 post Check whether a CSS property is supported.
That works for features like opacity
:
if ('opacity' in document.body.style)
Navigator user agent string information
Getting even more specific, I could add a classic navigator user agent string condition. We could use regular expression matching or the more lightweight regex test()
method, String.includes()
or the classic backwards compatible indexOf
idiom.
Deprecated browser detection ⚠️🚫
if (!!navigator.platform.match(/iPhone/))
if (/iPhone/.test(navigator.userAgent))
if (navigator.userAgent.includes('iPhone'))
if (navigator.userAgent.indexOf('iPhone') > -1)
While that's more readable than other iOS detection strategies and it perfectly fits my specific use case, as iOS
has been removed from the user agent string since Safari 13, this is still a hacky strategy, and if we do it for a legitimate reason, we should document why. I might even add a "TODO" comment, hoping a fellow developer will replace my code with a more elegant fix or workaround!
const supportsReducedMotion = (
CSS &&
(typeof(CSS.supports)==='function') &&
CSS.supports(
'prefers-reduced-motion: reduce'
)
);
const prefersReducedMotionQuery = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
const prefersReducedMotion = (prefersReducedMotionQuery.matches);
// detect iPhone up to Safari 12 to work around scroll stopper bug:
const isDeprecatedIphone = navigator.userAgent.indexOf('iPhone') > -1);
// TODO replace deprecated iPhone condition with more elegant bugfix
if (
!prefersReducedMotion ||
(!supportsReducedMotion && isDeprecatedIphone)
) {
Conclusion – Call to Action
Are you the one who knows the scroll stopper bug and a more elegant workaround (or the proper way to allow for horizontal and vertical scrolling inside a parallax perspective section on iPhones before Safari 13)?
Do you have a better best practice for bug(fix) detection?
Tell me in the comments!
Top comments (0)