Same Markup: Explaining "@_jscript_version" and Styling New HTML5 Elements

Last month I posted general guidelines for writing cross-browser code. Specifically I emphasized that feature detection (rather than browser detection) is better for working around differences between browsers. This is because feature detection automatically adapts to what a given browser supports and thus deals gracefully with new releases. Browser detection requires researching the level of support in every version of every browser and must be updated each time a new browser is released. Today I want to dig into a real-world example of this from some recent discussions. The conversation began around a piece of code like the following:

 

 // DON'T USE THIS
/*@cc_on
    @if( @_jscript_version < 9 )
        // Enable styling of new HTML5 elements
        ...
    @end
@*/

The goal of this code is to detect and work around a missing feature. In this case the feature is styling new HTML5 elements, or more specifically, using CSS to customize the display of new HTML5 elements. As written, this code fails to run in IE9 Compatibility View and ends up leaving new HTML5 elements without styles. In IE9 Standards Mode this is not a problem as IE9 enables styling for all elements by default.

The first problem that leads to this effect is the use of JScript’s conditional compilation which is similar to conditional comments. Both of these are forms of browser detection and should generally be avoided, especially when targeting the latest version of a browser. The second and more serious problem in this code is an attempt to use the @_jscript_version statement to detect the document mode of the page. The @_jscript_version statement is actually an indicator of which version of JScript is in use by the browser as a whole. In ALL document modes of IE9, this statement currently equates to “9”. In ALL document modes of IE8 it equates to “5.8” and in IE7 it is “5.7”. Thus it is not the correct piece of information to use to determine document mode.

Fortunately, developers can determine document mode directly and robustly. A simple DOM API was introduced in IE8 that provides exactly this information: document.documentMode. Modifying the original code to make use of this API results in the following:

 // Better, but still don't use
// Avoid in favor of feature detection
if(document.documentMode < 9) {
    // Enable styling of new HTML5 elements
    ...
} 

This is better, but the best way to achieve the original goal is detecting directly if styling of HTML5 elements is available, and avoiding browser sniffing altogether.

Here’s how to do it:

 // DO THIS: Feature detection for styling unknown elements
var elm = document.createElement("div");
elm.innerHTML = "<foo>test</foo>";
if(elm.childNodes.length !== 1) {
     // Enable styling of new HTML5 elements
     var elms = [
          "abbr","article","aside","audio","canvas","command",
          "datalist","details","figcaption","figure","footer",
          "header","hgroup","mark","meter","nav","output",
          "progress","section","summary","time","video"
     ];
     for(var i = 0; i < elms.length; i++) {
          document.createElement(elms[i]);
     }
}

Here’s how it works.

New HTML5 elements can't be styled by default in versions of IE prior to IE9 for two reasons. The first is because new HTML5 elements are treated as unknown elements. The second is that earlier versions of IE collapse all unknown elements at parse time. Being collapsed simply means that all children of an element actually become children of that element's parent. Furthermore, collapsed elements end up with separate entries in the DOM for their start and end tags. The following sample code and resulting DOM representations illustrate this point:

var elm = document.createElement("div");elm.innerHTML = "<foo>test</foo>";

Resulting DOM in IE8

 - <DIV>
    - test
    - </FOO>

Resulting DOM in IE9

 - <div>
    - <foo>
        - test

As you can see, the statement elm.innerHTML = "<foo>test</foo>" actually results in two children instead of one in browsers that collapse unknown elements. This behavior can be easily identified using feature detection (e.g. testing if elm.childNodes.length !== 1).

The final piece is understanding that enabling the styling of an unknown element is as simple as calling document.createElement("unknownElementName") from script before any element of that type is encountered by the HTML parser. Thus enabling the styling of new HTML5 elements involves calling document.createElement for each new element defined by the HTML5 spec.

This approach is an excellent example of the benefits of using feature detection instead of browser sniffing. Using the above code eliminates worrying about which version of which browser supports styling new HTML5 elements. Since the code tests for the behavior itself, it will automatically apply the appropriate workaround if and only if the workaround is actually needed.

Tony Ross
Program Manager

Edit 2:28pm: edit in the third code sample.

Edit 6/11 - correction in IE8 resulting DOM description.

Comments

  • Anonymous
    June 10, 2010
    The comment has been removed

  • Anonymous
    June 10, 2010
    Wouldn't having a @_HTML5_feature be better? Or @_HTML_version < 5

  • Anonymous
    June 10, 2010
    Mitch: Behavior detection can't handle every imaginable unexpected behavior.  But unlike user-agent sniffing, it stands a chance of working on browsers that you didn't specifically test on because they weren't yet released or you just didn't have the time.  Specifically, it'll work when the browser is close enough to one of the "behavior profiles" your code was meant to handle (i.e., either it styles HTML5 by default or it treats unknown elements the way legacy IE does). In the case of a recipe like the one Tony posted, it's been found to work on enough browsers to be quite practical, and it's pretty simple.  "Actually correct programming methods" involve a little compromise when you just don't know what environment will be running your code.

  • Anonymous
    June 10, 2010
    The comment has been removed

  • Anonymous
    June 10, 2010
    @Mitch 74: > other browsers may instead create an unknown inline block (akin to a span) > and fill it (because they are smart enough to recognize when a closing tag > matches an opening one), but not support any styling on an unnamed tag Do you have examples of this happening in practice? If so, then I agree that a more complete solution would need to take this into account. However I doubt applying the same workaround of calling document.createElement() would be sufficient and a new workaround would need to be found for such scenarios. > A correct solution would be to create the required element, determine what > kind of node it became, if the right one, try to style it, then read back > to see if its layout was actually affected. In this post I apply parser-oriented detection via innerHTML because, although the issue manifests itself as a lack of styling, the root cause is actually that the parser collapses the element.

  • Anonymous
    June 10, 2010
    Why don't you include the HTML5shiv script in the post? This is a widely used script for enabling HTML5 elements which recently has been updated with Jonathan Neal's script (www.iecss.com/print-protector), which also supports printing pages with HTML5 elements. The version you've listed on this page still breaks when printing any page using HTML5 tags.

  • Anonymous
    June 10, 2010
    @Tony, @Adrian: the goal of feature detection is to detect if a feature is supported. Now, it is reasonable to assume that some features do come together, and detecting each and every one of them is indeed impractical. However, in THAT current case, you're using innerHTML (which is implemented in varying ways across browsers) and the way unknown tags are created with it to do your object detection. Heck, your method would BREAK an XHTML 1.0 document (loaded with application/xhtml+xml MIMEtype)  because it attempts to create an entity unknown in the XHTML DTD! Yup, that one actually bit me in the *** on Chrome, Safari, Opera and your own IE 9 Developer Preview (Firefox didn't break, but then it might come to pass the day Mozilla decides to actually parse and enforce the DTDs on XHTML documents). So, what now? Well, don't use innerHTML. Use document.createElement('test'), and check 'test's nodeType. You can then set as many properties to it as you want (and check if they existed beforehand along the way), check their return values, then graft it onto the document's tree if it completes without errors. But at least this way you won't destroy the page if it fails hard somewhere. This may not be perfect, but it works on HTML and XHTML documents alike, it works on browsers that support HTML 4, it works on browsers that implement only a subset of HTML 5, and it will work on HTML 5 browsers. And, for all the beginner developers out there, it would be code that they would have to understand instead of cut'n'pasting and wondering why, for the life of 'em, this code works on IE browsers and not on LigntHTML5Browser v.1.5b used on that special cell phone. "But I found it on MSDN, and it works in IE!"

  • Anonymous
    June 10, 2010
    Why does _jscript_version evaluate to 9 rather than 5.9?  When you say it currently does so, does that mean this decision is yet to be finalised?

  • Anonymous
    June 10, 2010
    @Mitch 74 If you're creating new elements in an XHTML document, then of course you should add them to the DTD too. The article is about styling HTML5 elements. If you are using HTML5 elements then you should be using an HTML5 doctype, and not XHTML1.0. You can still use XHTML syntax when using an HTML5 doctype.

  • Anonymous
    June 11, 2010
    The comment has been removed

  • Anonymous
    June 11, 2010
    WHATWG has a little bit different recommendations on this: blog.whatwg.org/supporting-new-elements-in-ie blog.whatwg.org/supporting-new-elements-in-firefox-2

  • Anonymous
    June 11, 2010
    @John-David Dalton Thanks for the correction. I'll get the post updated shortly. On the note of this is not being supported in Firefox 2. It's true that this particular bit of detection and workaround doesn't fix Firefox 2's own styling issues, but as far as I'm aware the code does not cause any ill effect in Firefox either. The existing libraries definitely do a more complete job at covering the gotchas you pointed out and I won't attempt to duplicate that effort. My current concern is that nearly all of them have updated to use "if(@_jscript_version < 9)" since the first platform preview of IE9 was made available. As I point out in my post, this doesn't work they way they expect in IE9's compatibility view.

  • Anonymous
    June 11, 2010
    The comment has been removed

  • Anonymous
    June 11, 2010
    The comment has been removed

  • Anonymous
    June 12, 2010
    I do agree feature detection is the right approach than browser detection, but the given example is simply WRONG WRONG WRONG. I wonder why MS come up with such a stupid and fundamentally absurd example? Feature detection is to detect the feature to be used, or at least some relevant functions. You said your goal is detecting directly if styling of HTML5 elements is available, but what you are doing is detecting whether the browser is collapsing unknown elements, which are two completely unrelated things. So basically you are depending on the fact that those browsers that collapse unknown elements do not have styling of HTML5 elements, which means it's still a browser detection, not a feature detection. also conditional comments are still arguably the most reliable way to single out IE6 and IE7.

  • Anonymous
    June 13, 2010
    @wechrome: "I do agree feature detection is the right approach than browser detection, but the given example is simply WRONG WRONG WRONG." You are right, right, right!  :) They appear to have mistaken object inferences for feature detection (same as jQuery). "also conditional comments are still arguably the most reliable way to single out IE6 and IE7." Yes, but they should be used sparingly, typically to include proprietary IE styles like zoom, filter, etc.

  • Anonymous
    June 13, 2010
    @Tony "However I doubt applying the same workaround of calling document.createElement() would be sufficient and a new workaround would need to be found for such scenarios." And what leads you to doubt that?  It's obviously a better strategy for environments that feature browsers other than IE. Feature tests are supposed to be as simple and direct as possible.  It doesn't get much more simple and direct than appending one of the offending elements, styling it and then determining if the styles took effect. On the other hand, testing with innerHTML introduces many additional variables (not to mention that you are testing the wrong behavior). What you've done is simply a bad object inference, which is another form of browser sniffing popularized around the turn of the century. www.jibbering.com/.../detect-browser How can MS be so far behind on the Web?  Granted, Apple and Yahoo! are almost as clueless.  Can't any of these behemoths find good help in this area?

  • Anonymous
    June 14, 2010
    The comment has been removed

  • Anonymous
    June 15, 2010
    The comment has been removed

  • Anonymous
    June 16, 2010
    While philosophically not using browser detection is the right way to do things, the fact of the matter is that the only browser that needs detection is IE 95% of the time. I don't feel bad at all UA-sniffing to insert IE-specific code, and wrapping those in <[if IE lt 9]--> blocks, etc. If I only need to work around one browser, I'm not going to waste processing or delivery time for the rest of the browser market that plays nice.

  • Anonymous
    June 21, 2010
    browser sniffing is bad largely because of questionable reliability and lack of support for future versions, but IE's conditional comment is officially supported and reliable, so I personally don't see any problem using conditional comments to single out earlier versions of IE. Even for later versions of IE, it's still quite okay as long as you remember to send an X-UA-COMPATIBLE header.