CSS tends to be in a constant phase of transition as new specifications are continuously proposed, drafted, and then left to the browsers for implementation. How and when a new feature is implemented is determined by the browser, often including their vendor prefix (-moz-, -webkit-, -o-, -ms-) to further dilute the feature. In fact, sometimes the W3C will define an official specification for a feature after one or more browsers have already implemented it. Despite the emergence of CSS3 in both support and usage over the last couple years, it is still very much in the early stages of standardization and implementation which is often changing and debated over. To help combat the confusion, the following article will focus on methods of determining support not just for styles but also their supported assignable values.

Feature Queries

To help contend with this very problem, the W3C has drafted a new specification for an @supports rule. It’s used similarly to an if statement by providing a condition and nested CSS statements that are only rendered if the condition is met:

@supports (display: flex) {
    #content {
        display: flex;
    }
}

There is also an equivalent JavaScript API called CSS.supports or supportsCSS in Opera 12.1. This function supports two methods of usage by accepting either a property and value as separate arguments or a single conditional statement as the only argument:

CSS.supports('transform-origin', '5px');
CSS.supports('(display: table-cell) and (display: list-item)');

It is encouraging to know that the W3C seems to have acknowledged the discrepancies between browsers and their varying levels of CSS style support. Unfortunately the @supports rule is still very much in the early stages of adoption and is currently only supported in Firefox 22+, Chrome 28+, and Opera 12.1+. You can checkout the new API on MDN to read more.

Feature Testing Styles

Due to the lack of support for the @supports, we’ll require a JavaScript-based fallback solution to mimic its capabilities. Since browser detection is entirely unreliable and unnecessary, we need to find something better. Instead we will employ feature testing to ascertain the browser’s capability of computing the property and value we are trying to determine support for. The idea here is simple, if the browser can interpret the style than it must be supported, otherwise it must not.

For properties, examining the style object of an element and ensuring that the property exists will reliably resolve support in all mainstream browsers:

animationName in element.style;
transform in element.style;

That was easy, but to feature test support for assignable property values we need to get a little more creative. Manipulating an element’s style object is unreliable as it will have no effect for unsupported declarations and instead will behave like a simple object literal property-value assignment. Alternatively, adding the property and value as an inline style using an element’s style.cssText property will invoke the CSS interpreter to determine support. Then inspecting the style object will reveal that unsupported property values will always return an empty string:

element.style.cssText = "color:foo; width:10px";
element.style.color !== ""; // false
element.style.width !== ""; // true

Now that we can determine support for properties and their values, we can begin to formulate a solution.

The Solution

Throwing these techniques together while utilizing native implementations when available gives us the following function:

function isStyleSupported(prop, value) {
    // If no value is supplied, use "inherit"
    value = arguments.length === 2 ? value : 'inherit';
    // Try the native standard method first
    if ('CSS' in window && 'supports' in window.CSS) {
        return window.CSS.supports(prop, value);
    }
    // Check Opera's native method
    if('supportsCSS' in window){
        return window.supportsCSS(prop, value);
    }
    // Convert to camel-case for DOM interactions
    var camel = prop.replace(/-([a-z]|[0-9])/ig, function(all, letter) {
        return (letter + '').toUpperCase();                          
    });
    // Check if the property is supported
    var support = (camel in el.style);
    // Create test element
    var el = document.createElement('div');
    // Assign the property and value to invoke
    // the CSS interpreter
    el.style.cssText = prop + ':' + value;
    // Ensure both the property and value are
    // supported and return
    return support && (el.style[camel] !== '');
}

The isStyleSupported function accepts a style property as the first parameter in standard CSS notation (kebab-case or hyphenated format) and accepts the property value as an optional second argument. If no value is supplied, it will default to inherit as it is supported by all properties.

Leveraging Supported Styles

Despite the fact that this method proves to be a reliable polyfill for JavaScript, we still have no means of creating a substitute for the @supports rule in CSS. However, by adding classes to the <html> element that represent support for properties/values, we can utilize them in our stylesheets:

if(isStyleSupported('animation-name')){
    document.documentElement.classList.add('css-animation');
}

if(isStyleSupported('display', 'flex')){    
    document.documentElement.classList.add('flex-display');
}

Then we can create style declarations such as the following to take advantage of the feature in supporting browsers:

.css-animation #dialog {
    animation-name: popup;
    animation-duration: 1s;
}

.flex-display .column {
    display: flex;  
}

This type of solution is not ideal because of its obtrusive nature, violating the separation of concerns, but it’s a solution nonetheless.

Conclusion

With the help of this method we take advantage of new styles now, leveraging progressive enhancement when possible until support for the @supports API increases. For the latest version of this code, please refer to the project on GitHub.