JavaScript equivalent(s) of CSS @supports feature queries for bugfix detection
Tags: webdev, javascript, css, programming
Published: 2023-10-11 08:00:18

CSS media queries have an equivalent JavaScript function: [`window.matchMedia`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia). But what about `@supports` [feature queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_conditional_rules/Using_feature_queries)?

Take this code:

```javascript
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](https://en.wikipedia.org/wiki/IOS_version_history#12.5.5), 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 🌝🌚🤔🔦💡

```css
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](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/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"](https://en.wikipedia.org/wiki/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.

```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?

```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](https://developer.mozilla.org/en-US/docs/Web/API/CSS) object with a static [supports()](https://developer.mozilla.org/en-US/docs/Web/API/CSS/supports_static) method.

```javascript
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](https://developer.apple.com/documentation/safari-release-notes/safari-13-release-notes).

```javascript
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](https://lea.verou.me/blog/2009/02/check-if-a-css-property-is-supported/). 

That works for features like `opacity` :

```javascript
if ('opacity' in document.body.style)
```

### Navigator user agent string information

Getting even more specific, I could add a classic [navigator user agent](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent) string condition. We could use regular expression matching or the more lightweight [regex `test()` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test), [`String.includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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](https://stackoverflow.com/questions/9038625/detect-if-device-is-ios) 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!**

```javascript
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!