3 options to handle vendor-prefixed CSS properties in JS

JavaScriptUI — DevBlog #6

Bence Meszaros
5 min readJun 13, 2024
tl;dr

CSS vendor-prefixed properties are variations of CSS properties that browsers use to support experimental features before they become standard. They are mostly a thing of the past, but some of them are still in use and of course if you want to target older browsers you'll have to handle them in one way or another.

Recently, I came across them when trying to implement filters in JavaScriptUI. As it turns out, even the latest version of Safari lacks support for backdrop filters without the -webkit- prefix, and it got me thinking.

The dirty way

In pure CSS, the only way to deal with vendor prefixes is to throw everything at the wall and see what sticks:

-webkit-backdrop-filter: saturate(1.8) blur(20px);
-moz-backdrop-filter: saturate(1.8) blur(20px);
-ms-backdrop-filter: saturate(1.8) blur(20px);
-o-backdrop-filter: saturate(1.8) blur(20px);
backdrop-filter: saturate(1.8) blur(20px);

Whether you do this manually or use tools like Autoprefixer, your code will still ship four invalid properties along with each valid property, it will always apply four invalid properties for each valid property and if you use prefixer tools you even have an additional build step for all your CSS code. I don’t think you have to be a purist to find this annoying.

So what other options do we have?

#1 — The naive way

If CSS taps out, we need to move to JS. This is the whole idea behind JavaScriptUI.

The first and most obvious option is to simply mirror CSS in JS:

element.style.webkitBackdropFilter = "saturate(1.8) blur(20px)";
element.style.mozBackdropFilter = "saturate(1.8) blur(20px)";
element.style.msBackdropFilter = "saturate(1.8) blur(20px)";
element.style.oBackdropFilter = "saturate(1.8) blur(20px)";
element.style.backdropFilter = "saturate(1.8) blur(20px)";

This is a quick a dirty solution but doesn’t provide much benefit, as it suffers from the same issues and might even be more computationally expensive than its CSS counterpart. But it is an important stepping stone to demonstrate just how easy it is to mirror CSS in JavaScript.

The true power of JavaScript lies in the fact that, once we are in the realm of variables, functions, objects, and more, we have several options beyond simply mirroring behavior from CSS.

#2 — The polyfill way

One such option is to modify the CSSStyleDeclaration prototype based on the availability of vendor prefixed properties. First, we check which version of a property is available, then, if the standard is not available, create a property with the standard name, and finally reference the available vendor prefixed property from the newly created property.

Suppose that backdrop-filter is unavailable, but -webkit-backdrop-filter is. In that case we can do the following:

Object.defineProperty(
CSSStyleDeclaration.prototype,
"backdrop-filter",
Object.getOwnPropertyDescriptor(
CSSStyleDeclaration.prototype,
"-webkit-backdrop-filter"
)
);

Note, that properties on the CSSStyleDeclaration prototype are defined using accessor descriptors, thus handling them as simple properties won’t work.

In case you are worried about modifying the prototype of a built in object, you can define the property directly on an element instance or on an element wrapper object. Just make sure to use accessor descriptors when referencing a property from the CSSStyleDeclaration prototype.

The benefit of this approach is that once this code runs, we can simply use element.style.backdropFilter without any additional steps. The drawback is that this does not solve the issue of using the vendor prefixed name in other places, eg. in a transition value like this:

element.style.transition = "-webkit-backdrop-filter 0.2s ease-in-out 0.4s";

#3 — The variable way

A better approach would be to just store the available property name in a simple variable after feature detection and substitute it wherever needed. A basic example could be something like this:

let vendorBackdropFilter;

for (const property of ["backdrop-filter", "-webkit-backdrop-filter", "-moz-backdrop-filter", "-ms-backdrop-filter", "-o-backdrop-filter"]) {
if (document.documentElement.style[property] !== undefined) {
vendorBackdropFilter = property;
break;
}
}

element.style[vendorBackdropFilter] = "saturate(1.8) blur(20px)";

This is again just a naive implementation but you can improve this however you see fit:

  • you can wrap this snippet in an IIFE and use a const for storing vendorBackdropFilter
  • you can throw an error if no version has been found (or handle this case in any other way that makes sense in your code)
  • you can use a dummy div or other elements more specific to your code in place of document.documentElement
  • or even make a generic tester function that can check a list of properties used in your code:
function detect(...propertiesToTest) {
//implement feature detection logic
return availableProperties; //object or Map to store available property names
}

const vendor = detect("filter", "backdrop-filter", "transition");

Storing the names of available versions of properties in variables, constants or in any other data structure enables us to reference them uniformly throughout our code:

element.style[vendorBackdropFilter] = "saturate(1.8) blur(20px)";
element.style.transition = `${vendorBackdropFilter} 0.2s ease-in-out 0.4s`;

The more property names you use and the more places you use them, the harder it becomes with pure CSS and the easier it becomes using pure JavaScript.

Vendor prefixes in JavaScriptUI

Technically, there are two options to implement vendor prefixes in JavaScriptUI:

  • use getter/setter methods for each vendor prefixed property:
Text("Hello, World!")
.webkitBackdropFilter("saturate(1.8) blur(20px)")
.mozBackdropFilter("saturate(1.8) blur(20px)")
.msBackdropFilter("saturate(1.8) blur(20px)")
.oBackdropFilter("saturate(1.8) blur(20px)")
.backdropFilter("saturate(1.8) blur(20px)");
  • or handle them under the hood and expose only one getter/setter with the standard name:
Text("Hello, World!")
.backdropFilter("saturate(1.8) blur(20px)");

JavaScriptUI aims to fulfill two design principles: keep things simple and straightforward to minimize cognitive load for developers and maximize compatibility with existing web technologies. To achieve both, JavaScriptUI is using the second option and handles vendor prefixes under the hood. But it leaves the door open for using specific prefixes should the developer choose to do so.

This is achieved with a getter/setter called .css(), that accepts two arguments: the name of a CSS .style property and its value. This method acts as a getter if only the name of the property is supplied and a setter if both arguments are supplied. With this method, any CSS .style property is directly accessible if needed:

Text("Hello, World!")
.backdropFilter("saturate(1.8) blur(20px)")
.css("-webkit-backdrop-filter", "saturate(1.8) blur(20px)");

That’s a wrap

That’s all I have for you today. If you enjoy my work please clap, comment and share my work with others.

In any case, thank you for your time and have a great week.

⬅️ DevBlog #5 — 3 reasons why CSS shorthand properties are bad — and how to fix them

--

--

Bence Meszaros
Bence Meszaros

Written by Bence Meszaros

Lead Software Engineer, Fillun & Decketts

No responses yet