Feature Detection
and
Fallback




@pamelafox

feature-detection-fallback.appspot.com

HTML5: So Many Features!



*Thank you to this inappropriately named website for helping me making animated GIFs

.

HTML5: An Evolving Standard



From the WHATWG FAQ: When will HTML5 be finished?

The WHATWG is now using a Living Standard development model, so this question is no longer really pertinent.

The real question is, when can you use new features?

HTML5: Such varied browser support!

We need a strategy


  1. Research: What browsers does it work in?


  2. Detection: How do we know if it works in user's browser?


  3. Fallback: What do we do if it doesn't work?

Research:
What browsers does it work in?


Check caniuse.com, ES5 compatibility tables, mobilehtml5.org,
CSS3 selector support, mobile CSS support.

Pay attention to the notes.

Detection:
How do we know if it works?

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);
}

CSS Conditionals


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

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

Supported by: Opera, Chrome, FF



Public Service Announcement:

Don't Reinvent
The Wheel!

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);
}

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!

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');
          

Public Service Announcement:

Don't Reinvent
The Wheel!






*especially if the wheel involves regex.

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 */
}


Fallback:
What do we do if it doesn't work?

1) Shim it


shim: a wrapper library that lets you use new features in older browsers, automatically using older technologies when needed


Example FileAPI:

<input id="user-files" type="file" multiple />
<div id="preview-list"></div>
var previewNode = document.getElementById('preview-list');
  FileAPI.event.dnd(previewNode, function (over){
      $(this).css('background', over ? 'red' : '');
  }, function (files){
      // ..
  });



shim it, shim it good

2) Polyfill it


polyfill: A shim that mimics the interface of a future API.


Example: Array.forEach

var fruits = ['bananas', 'dugongs', 'unicorns'];
  fruits.forEach(function(fruit) { console.log(fruit + ' are good for you.')});
            
if ( !Array.prototype.forEach ) {
    Array.prototype.forEach = function(fn, scope) {
      for(var i = 0, len = this.length; i < len; ++i) {
        fn.call(scope, this[i], i, this);
      }
    };
  }

☞ What is a polyfill?
☞ How do you write a polyfill?

Public Service Announcement:

Don't Reinvent
The Wheel!

Finding shims and polyfills




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

3) Don't use it



If it's just a difference in styling, maybe you don't need to worry about it.

dowebsitesneedtolookexactlythesameineverybrowser.com

If it's a non-critical difference in functionality, ask users to upgrade.



True Stories!

What we wanted:

SVG Logos





Researching: SVG Logos


Detecting: SVG Logos



if (document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1")) {
  document.documentElement.className =  'supports-svg';
}

Using: SVG Logos



.coursera-logo {
  background: url('pages/home/template/coursera_logo_small.png');
  background-repeat: no-repeat;
  width: 108px;
  height: 17px;
  display: inline-block;
}

.supports-svg .coursera-logo {
  background: url('pages/home/template/logo.svg');
}


☞ CSS Tricks: Using SVG

What we wanted:

Rich Text Areas


HTML5 and rich text areas?



a contenteditable div?

caniuse.com:



...but we still need buttons!

Let's use a library!


WysiHTML5: Detecting support


function isContentEditableSupported() {
  var userAgent = this.USER_AGENT.toLowerCase(),
    hasContentEditableSupport = "contentEditable" in testElement,
    hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState,
    hasQuerySelectorSupport = document.querySelector && document.querySelectorAll,
    // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
    isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;

  return hasContentEditableSupport
    && hasEditingApiSupport
    && hasQuerySelectorSupport
    && !isIncompatibleMobileBrowser;
}

Plus: isTouchDevice, supportsSandboxedIframes, throwsMixedContentWarningWhenIframeSrcIsEmpty, displaysCaretInEmptyContentEditableCorrectly, hasCurrentStyleProperty, insertsLineBreaksOnReturn, supportsPlaceholderAttributeOn, supportsEvent, supportsEventsInIframeCorrectly, firesOnDropOnlyWhenOnDragOverIsCancelled, supportsDataTransfer, supportsHTML5Tags, canSelectImagesInContentEditable, clearsListsInContentEditableCorrectly, autoScrollsToCaret, autoClosesUnclosedTags, supportsNativeGetElementsByClassName, supportsSelectionModify, supportsClassList, needsSpaceAfterLineBreak, supportsSpeechApiOn, crashesWhenDefineProperty, doesAsyncFocus, hasProblemsSettingCaretAfterImg, hasUndoInContextMenu, ...

WysiHTML5: Falling back


<textarea id="wysihtml5-textarea"></textarea>
var editor = new wysihtml5.Editor("wysihtml5-textarea", {});


wysihtml5.Editor = function(textareaElement, config) {
  this.textareaElement  = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement;
  this.config           = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
  this.textarea         = new wysihtml5.views.Textarea(this, this.textareaElement, this.config);
  this.currentView      = this.textarea;
  this._isCompatible    = wysihtml5.browser.supported();
  
  // Sort out unsupported/unwanted browsers here
  if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
    var that = this;
    setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
    return;
  }
  this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config);
};

Public Service Announcement:

Shiny Things Are Fragile!

iOS contenteditable issues

Falling back in iOS

// We use wysi editor on desktop devices but not on iPad due to
  // many usability reports with inability to type, freezing, etc.
  function makeEditor($form) {

    var showToolbar = true;

    // Can be rich (wysi), html (html editing), or markdown
    var defaultComposer = 'rich';
    if (util.isIOS()) {
      defaultComposer = 'markdown';
    }
    // ...

What we wanted:

Videos

Researching: <video> tag


...but it's not that easy


From the HTML5 Spec:

It would be helpful for interoperability if all browsers could support the same codecs.
However, there are no known codecs that satisfy all the current players: we need a codec that is known to not require per-unit or per-distributor licensing, that is compatible with the open source development model, that is of sufficient quality as to be usable, and that is not an additional submarine patent risk for large companies.

Researching: video codecs

Researching: <track> tag


It's time to shim it!



Flash fallback? Track support? Extendible?




...we went with MediaElementJS

MeJS: Detecting codec support



  video.canPlayType: returns "probably", "maybe", "no", or "". 
  

A simplified snippet from MediaElementJS:


    // special case for Android which sadly doesn't implement the canPlayType function (always returns '')
    if (mejs.MediaFeatures.isBustedAndroid) {
      htmlMediaElement.canPlayType = function(type) {
        return (type.match(/video\/(mp4|m4v)/gi) !== null) ? 'maybe' : '';
      };
    }   

    for (i=0; i < mediaFiles.length; i++) {
      // normal check
      if (htmlMediaElement.canPlayType(mediaFiles[i].type).replace(/no/, '') !== '' 
        // special case for Mac/Safari 5.0.3 which answers '' to canPlayType('audio/mp3') but 'maybe' to canPlayType('audio/mpeg')
        || htmlMediaElement.canPlayType(mediaFiles[i].type.replace(/mp3/,'mpeg')).replace(/no/, '') !== '') {
        result.method = 'native';
        result.url = mediaFiles[i].url;
        break;
      }
    }
              

MeJS: Falling back to Flash


A simplified snippet from MediaElementJS:


          container.innerHTML =
'<embed id="' + pluginid + '" name="' + pluginid + '" ' +
'play="true" ' +
'loop="false" ' +
'quality="high" ' +
'bgcolor="#000000" ' +
'wmode="transparent" ' +
'allowScriptAccess="always" ' +
'allowFullScreen="true" ' +
'type="application/x-shockwave-flash" pluginspage="//www.macromedia.com/go/getflashplayer" ' +
'src="' + options.pluginPath + options.flashName + '" ' +
'flashvars="' + initVars.join('&') + '" ' +
'width="' + width + '" ' +
'height="' + height + '"></embed>';
      

MeJS: Using it


A simplified snippet from our code:

<video controls="controls"
   id="QL_video_element_first"
   style="width:100% important!;">
 <source type="video/webm" src="http://spark.s3.amazonaws.com/dugong/video.webm">
 <source type="video/mp4" src="http://spark.s3.amazonaws.com/dugong/video.mp4">
 <track kind="subtitles" srclang="en" src="https://class.coursera.org/dugong-002/lecture/subtitles?q=7_en">        
</video>

var options = {
  videoWidth: this.width,
  videoHeight: this.height,
  startVolume: 1.0,
  loop: false,
  enableAutoSize: false,
  features: mediaelementFeatures,
  translationSelection: false,
  alwaysShowControls: true};

new mejs.MediaElementPlayer(videoElement, options);

Public Service Announcement:

Double technology
=
double trouble


  • QA takes twice as long to test changes
  • There are twice the bug reports from students
  • There's twice the maintenance burden

HTML5 video support is unstable

Chromium Issue Tracker:

Flash has security issues

"canPlayType"
=
"can maybe kind of play type"



We have to give users the option:

It's tempting...

But is it worth it?

Do you have the time?


Do you have the resources?


Do you *want* to be the pioneer?


Do you have an alternative?


Good luck -
Share what you learn
with all of us!