Feature Detection

In the real world




@pamelafox, #io13

feature-detection-io.appspot.com

HTML5: A Blessing and a Curse



Hmm. I want to use X. But how will I decide when I can use X??

In an ideal world...

I know! I'll just ask the browser if they support it!

Feature Detection

JS APIs:
var supportsAudio = ("webkitAudioContext" in window || "AudioContext" in window);

HTML elements:
var div = createElement('div');
div.innerHTML = '<svg/>';
var supportsInlineSVG = div.firstChild
    && div.firstChild.namespaceURI == 'http://www.w3.org/2000/svg';

CSS:
var supportsTextShadow = 
     ("textShadow" in document.createElement("detect").style);

With Modernizr:
if (Modernizr.touch) {
   $('button').on('touch', handleClick);
}

So sweet when it works

I totally just detected the shit out of that feature!

But when it doesn't...

Oh noesss....it didn't work!

It's time for Plan B.

Sigh. Guess I'll have to sniff the user agent instead...

User Agent Sniffing

iPad/iPhone:
var isIOS = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i);
Top mobile browsers:
var isMobile = (navigator.userAgent.match(/(Android (1.0|1.1|1.5|1.6|2.0|2.1))|(Windows Phone (OS 7|8.0))|(XBLWP)|(ZuneWP)|(w(eb)?OSBrowser)|(webOS)|(Kindle\/(1.0|2.0|2.5|3.0))/));
All mobile browsers:
(function(a,b){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))window.location=b})(navigator.userAgent||navigator.vendor||window.opera,'http://detectmobilebrowser.com/mobile');
        

Feature Detection
vs.
User Agent Sniffing


True stories!

The case of the
hard-to-use localStorage

localStorage

A client-side key-value storage API

Store data:
$('form').on('submit', function() {
  window.localStorage.set('username', $('input').val());
});
Retrieve data:
alert(window.localStorage.get('username'));

Detecting support


function supportsStorage() {
  try {
    return 'localStorage' in window && window['localStorage'] !== null;
  } catch (e) {
    return false;
  }
}

Detecting support, Take 2


function supportsStorage() {
  var key = '__lscachetest__';
  // It's not straightforward due to FF4 issues and quota detection.
  try {
    // Fix for iPad issue - sometimes throws QUOTA_EXCEEDED_ERR on setItem
    localStorage.removeItem(key);
    localStorage.setItem(key, key);
    localStorage.removeItem(key);
    return true;
  } catch (exc) {
    return false;
  }
}


☞ lscache.js


☞ Modernizr: localstorage.js

Detecting support, Take 3


var cachedSupportsStorage;

function supportsStorage() {
    var key = '__lscachetest__';
    var value = key;

    if (cachedSupportsStorage !== undefined) {
      return cachedSupportsStorage;
    }

    try {
      setItem(key, value);
      removeItem(key);
      cachedSupportsStorage = true;
    } catch (exc) {
      cachedSupportsStorage = false;
    }
    return cachedSupportsStorage;
}

What I Learnt

  • Don't just check that a browser implements an API...
    Check that the browser lets you use the API the way you want to.

  • Don't let feature detection slow down your site.


My Browser Wishlist

Browsers should provide functions that let you know quickly whether an API can be used how you want it.

For example:

localStorage.canSet();
localStorage.canGet();

The case of the
missing FormData

FormData

A set of key/value pairs representing form fields and their values

var formData = new FormData (document.forms[0])


☞ MDN: FormData

Detecting support


if (window.FormData) {
  var formData = new FormData(form[0]);
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/action/' + target, true);
  xhr.onload = function(e) {
    var responseJSON = JSON.parse(this.responseText);
    callback(processJSON(responseJSON));
  };
  xhr.send(formData);
} else {
  $.ajax({
      url: url,
      type: 'POST',
      data: form.serialize(),
      dataType: 'json', success: function(responseJSON) {
        onSuccess(processJSON(responseJSON));
      }
  });
}

Sad Safari users


I can't signup on Safari!


Detecting Safari



function isSafari() {
  return ($.browser.webkit && !(/chrome/.test(navigator.userAgent.toLowerCase())));
} 

function sendForm(form, target, callback) {
  if (window.FormData && !isSafari()) {
    var formData = new FormData(form[0]);
    // ...
  } else {
    // ...
  }
}

What I Learnt

  • Be wary of using new APIs...
    Especially for mission-critical parts of your app.

  • Its safer to use the old APIs/libraries, with their known issues.


My Browser Wishlist

All APIs that store/transmit data should have accessors to check whether they work as expected.

For example:

var formData = new FormData(form);
  if (!formData.hasItem('username')) {
    // use older technique
  }

The case of the
fancy CSS

CSS3 Styling

.round-glossy-button {
  border-radius: 3px;
  background: linear-gradient(to bottom, #f0f9ff 0%,#cbebff 47%,#a1dbff 100%); 
  box-shadow: 2px 2px 3px #888;
}

Sad, sad Android users


Making Android happy

.modal {
    @include box-shadow(none);
    @include background-clip(border-box);
    @include border-radius(0px);
    border: 1px solid black;
}

Detecting Android


function inUserAgent(str) {
  return (new RegExp(str)).test(navigator.userAgent.toLowerCase());
}

function isAndroid() {

  var isAndroidOS = inUserAgent('android') || inUserAgent('HTC_') || inUserAgent('Silk/');

  var isAndroidPG = (window.device && window.device.platform && window.device.platform == 'Android' || false;

  return isAndroidOS || isAndroidPG;
}

if (isAndroid() || getUrlParam('os') == 'android') {
  $('body').addClass('android');
}
.android .modal {
    @include box-shadow(none);
    @include background-clip(border-box);
    @include border-radius(0px);
    border: 1px solid black;
}

What I Learnt

  • Just because something "works", it doesn't mean it works well.

  • Be careful about using new features in bulk, as any performance problems will be exacerbated.


My Browser Wishlist

Browsers should know their limits, and not render CSS features they can't handle.

The case of the
signup process

Signature Track

The Requirements


Physical keyboard

Our first attempt:

function isTouchSupported() {
    return (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch);
}

...but that didn't always work, so...

function isMobileDevice() {
    var ua = navigator.userAgent || navigator.vendor || window.opera;
    return (/iPhone|iPod|iPad|Android|BlackBerry|Opera Mini|IEMobile/).test(ua);
}

An aside: Touch Events

Cross-browser touch detection:

var supportsTouch = (('ontouchstart' in window) ||
     (navigator.maxTouchPoints > 0) ||
     (navigator.msMaxTouchPoints > 0));
var clickEvent = ('ontouchstart' in window ? 'touchend' : 'click');
            

But supportsTouch != onlySupportsTouch!

To support both/either:

blah.addEventListener('touchend', function(e) {
  e.preventDefault();
  e.target.click();
})
blah.addEventListener('click', function() {
});
Read more: Supporting touch: the why, not the how, You can't detect a touchscreen

Flash Plugin


Using SWFObject:

<script src="swfobject.js"></script>
function isFlashSupported() {
  swfobject.hasFlashPlayerVersion("8")
}

Webcam

No way to detect without asking user, so...

What I Learnt

If you block users based on lack of features, tell them why.




My Browser Wishlist

Give us a way to detect physical devices and plugins.

navigator.hasPhysicalKeyboard();
navigator.hasWebcam();
navigator.hasPlugin('Flash', '8.0');

Reminder: Why this matters

Unequal feature distribution is the reality:

BrowserStats, caniuse visualizations

Browsers will help us


Introducing CSS conditionals, level 3:


@supports (column-count: 1) and (background-image: linear-gradient(#f00,#00f)) {
            }

var foo = window.supportsCSS('column-count: 1');

Supported by: Opera, Chrome, FF



...but there's still much to do.

Do not assume it's going to be easy


Do not just copy the first StackOverflow answer


Do use tried and true techniques

Prefer popular, well-tested libraries
(# of forks, issues, tests, browser coverage, last updated)

If you're not sure, ask!
(#jquery, Modernizr issues, @paul_irish)



Do try feature detection first

Tried & True: Modernizr

A small JS library that detects the availability of native implementations

Load the Modernizr JS:
<script src="/i/js/modernizr.com-custom-2.6.1-01.js"></script>
Modernizr runs tests, adds class names and JS properties:
<html class="js no-touch postmessage history multiplebgs boxshadow opacity cssanimations csscolumns cssgradients csstransforms csstransitions fontface formdata">

Then you can use in CSS:
html.svg .logo {
  background-image: url('logo.svg');
}
Or in your JS:
if (Modernizr.touch) {
   $('button').on('touch', handleClick);
}

Tried & True: ua-parser

An OSS project to collect user agent detection code.


regexes.yaml

Python, JavaScript, PHP, Ruby, Java, D, C#, Perl


<script src="ua-parser.js"></script>
var result = uaparser.parse(navigator.userAgent);
console.log(result.ua.toString());        // -> "Safari 5.0.1"
console.log(result.ua.toVersionString()); // -> "5.0.1"
console.log(result.ua.family)             // -> "Safari"
console.log(result.ua.major);             // -> "5"
console.log(result.ua.minor);             // -> "0"
console.log(result.ua.patch);             // -> "1"

Created by BrowserStack.
Used by Google, Facebook (and you?).

Mix & Match


Feature detect, then blacklist:

if (Modernizr.touch && !(ua.device == 'Safari' && ua.major == '4') {
  /* do something that requires touch but is known to not work in Safari 4 */
}

We don't have this...

but we do have:

and: