You are reading a translation of an old blog post published on my previous blog in French.

[-prefix-free is] fantastic, top-notch work! Thank you for creating and sharing it.

— Eric Meyer

Thanks to -prefix-free, you no longer have to write CSS properties for every browser extension. You only need to write CSS standard properties, and your code is still supported by all browsers. It is not surprising that most code playgrounds now provide this essential library. Let’s take for example the following pen created initially by amos.

See the Pen cblAm by Julien Sobczak (@julien-sobczak) on CodePen.

No prefix -moz or -webkit. Under the hood, -prefix-free adds the prefixed properties dynamically and only if necessary depending on your browser. How does this magic work? We will discover it in this article.

-prefix-free is brought to us by Lea Verou and is available on Github. The code presented in this article has been simplified for obvious reasons and must not be used outside this learning context. This article is based on the last version at the moment of the publication of this article.

A First Example

/* demo.css */
h1 {
  background: orange;
  border-radius: 10px;
}

And:

<!-- demo.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>PrefixFree</title>

    <link rel="stylesheet" href="demo.css">
  </head>
  <body>

    <h1>Hello World!</h1>

    <script src="http://cssdeck.com/assets/js/prefixfree.min.js"></script>

  </body>
</html>

If we inspect the source code inside our browser, on observe small differences:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- demo.html (using Firefox 3.6) -->
<!doctype html>
<html class="-moz-">
  <head>
    <meta charset="utf-8">
    <title>Démo PrefixFree</title>

    <style media="" data-href="demo.css"> (1)
      h1 {
        background: orange;
        -moz-border-radius: 10px;
      }
    </style>
  </head>
  <body>

    <h1>Hello World!</h1>

    <script src="http://cssdeck.com/assets/js/prefixfree.min.js"></script>

  </body>
</html>
1 The stylesheet has disappeared and has been replaced by an inline stylesheet. The content is identical with one exception: the use of the prefix -moz for the still unsupported property border-radius.

Let’s Go!

-prefix-free declares two global variables (StyleFix and PrefixFree), reflecting how the library is organized around two distinct parts:

  • StyleFix is a framework to apply corrections on a CSS stylesheet.

  • PrefixFree relies on it to configure a custom corrector that replace unsupported CSS properties using the prefixed properties instead.

We are using the property border-radius for illustration purposes in this article. This property is available (with the -moz prefix) since the version 2 of Firefox. The following examples have been tested using Firefox 3.6.

StyleFix

StyleFix applies a series of changes that are called fixers. A fixer is basically a function satisfying the following signature:

var css = fix(css, raw, element);

Where:

  • css is a string containing the CSS code to fix.

  • raw is false when the CSS is directly attached to an HTML tag.

  • element is the associated HTML element (<link>, <style> or the HTML tag HTML with the attribute style).

The function returns the modified CSS.

The registration of a fixer is done using the function register:

var self = window.StyleFix = { // Définition de l’objet global

  register : function(fixer) {
    self.fixers = (self.fixers || []).push(fixer);
  },

};

The fixers are then triggered for every stylized element through the function fix:

fix : function(css, raw, element) {
 for (var i = 0; i < self.fixers.length; i++) {
  css = self.fixers[i](css, raw, element);
 }

 return css;
}

Nothing too complicated until now.

Now let’s take a look at what happens when the page is loading. After the DOM is parsed by the browser, StyleFix looks for all tags <link>, <style> and the ones declaring the attribute style. For our implementation, we are going to consider only the tags <style> but the logic is unchanged for other types of tags.

var self = window.StyleFix = {

 styleElement : function(style) {
  style.textContent = self.fix(style.textContent, true, style);
 },

 process : function() {
  [].forEach.call(document.querySelectorAll('style'), StyleFix.styleElement);
 },

};

document.addEventListener('DOMContentLoaded', StyleFix.process, false);
Zoom on querySelectorAll

The small subtlety of this code comes from the method querySelectorAll that returns an object NodeList. This object supports a property length and can be traversed using a for loop, except that we cannot use the common method forEach. Why? NodeList is not an array and we need to use a small hack like [].forEach.call(…​) to fix that (see the NodeList documentation).

We have finished with the object StyleFix. Here is the final implementation:

(function() {

  var self = window.StyleFix = {

   styleElement : function(style) {
    style.textContent = self.fix(style.textContent, true, style);
   },

   process : function() {
    [].forEach.call(document.querySelectorAll('style'), StyleFix.styleElement);
   },

   register : function(fixer) {
    (self.fixers = self.fixers || []).push(fixer);
   },

   fix : function(css, raw, element) {
    for (var i = 0; i < self.fixers.length; i++) {
     css = self.fixers[i](css, raw, element);
    }

    return css;
   }

  };

  document.addEventListener('DOMContentLoaded', StyleFix.process, false);
})();

Before moving on next section, here is an example of how to use it to convert all stylesheets using a single line:

StyleFix.register(function(css, raw, element) {
 return css.replace(/\n/gm, '');
});

PrefixFree

If we omit many implementation concerns, we can start with a first operational, minimal version:

StyleFix.register(function(css, raw, element) {
 var prefix = '-moz-', (1)
     properties = ['border-radius']; (2)

 for (var i = 0; i < properties.length; i++) {
  var regex = RegExp(properties[i], 'gi'); (3)
  css = css.replace(regex, prefix + properties[i]);
 }

 return css;
});
1 We focus on Firefox 3.6 for now.
2 We consider only the property border-radius.
3 We search for every property to replace.

The code reuses the object StyleFix to register a custom fixer. This fixer replaces unsupported CSS properties with their equivalent. The regular expression allows making a global replacement. In JavaScript, the method replace only replaces the first occurrence (a flag can be defined as the third argument but is currently not supported by the V8 engine).

If we want to run our code on new code, we still have to solve two remaining issues:

  • How to detect the browser prefix to use?

  • How to identify the properties to replace?

Let’s start with the first question.

Several solutions are possible. We may use Modernizr but -prefix-free use an even simpler solution. The code creates a new HTML element in the DOM and inspects the attribute style represented in JavaScript by the object CSSStyleDeclaration. This object lists the values of all CSS properties supported by the browser. So, we just have to memorize the list of all properties starting with - to determine the prefix and answer the second question by the same token.

var prefix = undefined,
  properties = [],
  dummy = document.createElement('div').style;

for (var property in dummy) {
 property = deCamelCase(property); (1)

 if (property.charAt(0) === '-') {
  properties.push(property);

  prefix = prefix || property.split('-')[1];
 }
}

self.prefix = '-' + prefix + '-';
1 This line is necessary to find the name of the property as present in CSS. Indeed, in JavaScript, the CSS properties as defined as properties in the object CSSStyleDeclaration, and thus must conform to the rules of the JavaScript language (- is not allowed in an identifier).

We defined two utility functions to convert from one notation to the other:

function camelCase(str) {
 return str.replace(/-([a-z])/g, function($0, $1) {
  return $1.toUpperCase();
 }).replace('-', '');
}

function deCamelCase(str) {
 return str.replace(/[A-Z]/g, function($0) {
  return '-' + $0.toLowerCase()
 });
}

In the previous example, we used an array of all CSS properties with a prefix. We still have one case to manage: browsers evolve, and standard CSS properties become supported over time (ex: Firefox >= 4 supports both -moz-border-radius and border-radius properties). When the standard property is supported, we better had to use it and stop replacing it.

// (suite)
// var properties = [/* all properties with a supported prefix */]

self.properties = [];

supported = function(property) {
 return camelCase(property) in dummy;
}

// Get properties ONLY supported with a prefix
for (var i = 0; i < properties.length; i++) {
 var property = properties[i];
 var unprefixed = property.slice(self.prefix.length);

 if (!supported(unprefixed)) {
  self.properties.push(unprefixed);
 }
}

Our rewrite of PrefixFree is now complete:

(function(root) {

 function camelCase(str) {
  return str.replace(/-([a-z])/g, function($0, $1) {
   return $1.toUpperCase();
  }).replace('-', '');
 }

 function deCamelCase(str) {
  return str.replace(/[A-Z]/g, function($0) {
   return '-' + $0.toLowerCase()
  });
 }

 var self = window.PrefixFree = {
  prefixCSS : function(css, raw, element) {
   var prefix = self.prefix;

   for (var i = 0; i < self.properties.length; i++) {
    var regex = RegExp(self.properties[i], 'gi');
    css = css.replace(regex, prefix + self.properties[i]);
   }

   return css;
  }

 };

 (function() {
  var prefix = undefined,
    properties = [],
    dummy = document.createElement('div').style;

  supported = function(property) {
   return camelCase(property) in dummy;
  }

  for ( var property in dummy) {
   property = deCamelCase(property);

   if (property.charAt(0) === '-') {
    properties.push(property);

    prefix = prefix || property.split('-')[1];
   }
  }

  self.prefix = '-' + prefix + '-';

  self.properties = [];

  // Get properties ONLY supported with a prefix
  for (var i = 0; i < properties.length; i++) {
   var property = properties[i];
   var unprefixed = property.slice(self.prefix.length);

   if (!supported(unprefixed)) {
    self.properties.push(unprefixed);
   }
  }

 })();

 StyleFix.register(self.prefixCSS);

})(document.documentElement);
Congratulations!

We have finished the coverage of -prefix-free. Less than 100 lines of code have been necessary to recreate a basic implementation. The complete source code is available here.

Try for yourself!
  • Try to support the tags <link> and CSS properties defined using the HTML attributes style. Hint: Retrieve the content of external stylesheets in AJAX. What are the limitations?

  • Try to support CSS changes done in JavaScript after the initial loading of the page. Hint: Listen events DOMAttrModified and DOMNodeInserted (see the plugin prefixfree.dynamic-dom.js).

  • Try to support @rules and keyframe. Hint: Use more advanced regular expressions.

To Remember
  • StyleFix/PrefixFree is a great example of the approach divide-and-conquer.

  • querySelectorAll returns an object of type NodeList, which is different from Array.

  • The object CSSStyleDeclaration can be used to list all CSS properties supported by a browser.

About the author

Julien Sobczak works as a software developer for Scaleway, a French cloud provider. He is a passionate reader who likes to see the world differently to measure the extent of his ignorance. His main areas of interest are productivity (doing less and better), human potential, and everything that contributes in being a better person (including a better dad and a better developer).

Read Full Profile

Tags