When you’re building a web site, you want it to look great
in any browser…and ideally, it should look great for a long time, even in
future versions of the browsers. I’ve pulled together some hints, tips and best
practices that will help your sites look their best.
A Bit of History
Today, all web browsers are built with one common goal in
mind: Render webpages optimally according to the latest specifications.
This wasn’t always so. In the past, most browsers
implemented high-demand features that were not yet standardized. Each browser
did so in its own way, like, for example, the possibility to set transparency
in CSS.
Internet Explorer, before v.8, understood the following CSS:
.transparent {
width: 100%;
filter: alpha(opacity=50);
}
Firefox had its own attribute:
.transparent {
-moz-opacity:0.5;
}
As did Safari:
.transparent {
-khtml-opacity: 0.5;
}
Today, however, in CSS3, there is a unified way of setting
the transparency of an element:
.transparent {
opacity: 0.5;
}
It can certainly be seen as a good thing when browsers go
the "extra mile" to support new features quickly. But when those features
aren’t standardized, it definitely makes life harder for developers, since we
have to account for all the different implementations of each feature.
Same Markup
The best way to make sure your web page will render
optimally in all browsers is to use markup that is supported in all current browsers.
Until very recently, this was HTML 4.01, a 10-year-old standard with very
limited features.
Today, all browsers are converging toward the feature-rich HTML5.
However, many of the new specifications that fall under the term "HTML5" (including
HTML5 markup, its APIs including DOM Levels 2 and 3, CSS3, SVG, and EcmaScript
262) are still in development and subject to change.
Browser vendors are continuously adding support for HTML5
features, but at quite different paces.
Firefox and Opera are usually quick to adopt new
HTML5 specifications—even some that are in early development, subject to
change, or have security issues.
On the upside, it's great that developers get a chance to
test new features. However, this early adoption rate often leads to sites that
use features that break pages between browser releases. An example of this
was Firefox 4 disabling Websockets
between Beta 7 and 8 due to security reasons. It's an
extremely frustrating experience for both users and developers
Chrome—which is also quickly adopting new HTML5 standards—has
recently managed to stir the HTML5 community with its announcement
to abandon support for the popular h.264 video codec for HTML5 <video>
elements and instead switch to the royalty free WEBM standard. While this is
not a bad decision for developers that currently pay for h.264 licenses, it
again adds more choices that developers will have to keep track on what video
formats they will need to encode and host their content in order to support as
many browsers as possible.
Microsoft doesn’t implement standards as quickly, but does
work closely with the W3C to build test suites. This collaboration helps to:
-
Minimize ambiguity in the specifications
-
Build technical foundations for browser vendors to work on
-
And homogenize how browsers render HTML5.
To see their latest work in this area, have a look at Internet
Explorer 10 Platform Preview that’s available on IE Test Drive.
You can also check out the HTML5labs, where Microsoft prototypes early and unstable
specifications from web standards bodies such as the W3C. See Improved
Interoperability Through Standards Support for in-depth information on how
Internet Explorer 9 supports the various HTML5 specifications today.
But since the new HTML5 standards are still a moving target—and
most Internet users don’t use the latest version of any given browser—serving the
right markup is more important than ever.
Browser Detection
One approach to cover browser differences is by using browser
detection. The most common approach is to use JavaScript to query the user-agent
header:
<script type="text/javascript">
if ( navigator.userAgent.indexOf("MSIE")>0 )
{
// run custom code for Internet Explorer.
}
</script>
But there are a couple of problems with doing it this way.
First, it bundles multiple assumptions of which features the
browser supports in one single check. A single wrong assumption can break the
site. So, as a developer, you have to keep track of which features are
supported by which browsers.
The second issue is that the browser check above doesn’t
take browser versions into consideration, so it isn’t future-proof. So even
if it works with today’s version of a browser, the next release might
not require—or worse, might remove support altogether for—a workaround that the
browser detection was used to add to the site.
If you have to use browser detection, make sure to only
use it to detect legacy browsers as shown in the following example, and
always keep in mind the version of the browser you are checking for.
<script type="text/javascript">
function getInternetExplorerVersion()
// Returns the version of Internet Explorer or a -1 for other browsers.
{
var rv = -1;
if (navigator.appName == {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null)
rv = parseFloat( RegExp.$1 );
}
return rv;
}
function onLoad()
{
var version = GetInternetExplorerVersion()
if (version <= 7 && version > -1)
{
// Code to run in Internet Explorer 7 or less.
}
}
</script>
MSDN has a great article with more information if you’re
interested: "Detecting
Browsers More Effectively".
JavaScript
Tutorials has a comprehensive article on how to use the navigator object and
regular expressions to detect the various browsers and their exact versions.
Another way to detect browsers is by using Internet
Explorer’s conditional comments. This syntax extends the standard HTML
comments and is unique to Internet Explorer since version 5.
One way conditional comments can be used is with CSS style
sheets. You can have certain IE-specific CSS rules—rules that you want other
browsers to ignore. In the following sample, an "ie7.css" is loaded only if
Internet Explorer 7 or less is detected.
<!---->
You can read more detailed information on all the
capabilities of conditional comments over at MSDN: "About
Conditional Comments".
Given all the problems and limitations of browser detection,
let’s look at an alternative.
Feature Detection
The far better approach to handling differences in web
browsers in your page is to use feature detection.
Before using a feature that you know has different
implementations in the various browsers, you can run a small test where you
look for the availability of a specific object, method, property, or behavior.
In most cases, this can be done by trying to create a new
instance of the feature in question. If that instantiation returns something
other than null, the executing browser knows the feature. If not, you can test
to see if there’s a workaround or proprietary legacy implementation of the
feature available.
Comparing Browser and Feature Detection
Sometimes diagrams help clear up the differences between the
two in various situations.
These are all the possible code paths that lead through our
test site.
Well-known browser configurations
When faced with well-known browser configurations, both work.
But browser detection has the fixed assumption that both Feature A and Feature
B are supported by the browser, whereas feature detection tests for each
feature individually.
Unknown browser configuration
Things get really interesting when faced with an unknown
browser configuration.
Feature detection handles things well and finds out that
this browser is capable of displaying Feature A but needs fallback code for
Feature B. Browser detection picks a path because it only checks the browser’s
name, or it simply chooses the default path when none of the queried browser/version
combinations match.
Either way, in this example, the page will not render
properly with browser detection because there is no code path that connects all
the valid code segments, even though the page would have contained all the code
necessary to properly display in this unknown browser configuration.
Feature Detection Examples
There are two very important recommendations to keep in
mind when using feature detection.
First, always test for standards, as a browser often
supports the newer standard as well as the legacy workaround.
Second, only target related features in a single check,
to minimize assumptions about a browser’s capabilities.
Let’s look at a few examples of feature detection.
The following script creates two code paths. It first checks
if the browser supports window.addEventListener, and if not, probes for
the availability of the legacy feature window.attachEvent.
<script type="text/javascript">
if(window.addEventListener) {
// Browser supports "addEventListener"
window.addEventListener("load", myFunction, false);
} else if(window.attachEvent) {
// Browser supports "attachEvent"
window.attachEvent("onload", myFunction);
}
</script>
Another good approach is to capsule feature detection into a
set of functions that can then be used throughout the code. Here is a best
practice on how to detect if the browser supports the HTML5 <canvas>, and
if yes, makes sure that the canvas.getContext(‘2d’) method is working as well.
It simply returns "true" or "false," making it easy to reuse.
<script type="text/javascript">
function isCanvasSupported()
{
var elem = document.createElement( return !!(elem.getContext && elem.getContext( }
</script>
When you’re using feature detection, always use it on newly
created elements or objects. This helps you rule out the possibility that any
other script on the page has modified it since it was created, which could lead
to random or erratic results.
Feature detection also works directly for a select few HTML elements
such as HTML5's <video>, <audio>, and <canvas> in form of a
"fallback". The browser displays the first supported sub-element from the top
and visually hides the elements below.
The easiest form looks like this:
<video src="video.mp4">
Your browser doesn't support videos natively.
</video>
A browser supporting the <video> element will show the
video "video.mp4" and a browser that doesn’t will fall back to the supplied text.
But fallback also works for various video formats in the video tag:
<video>
<source src="video.mp4" type="video/mp4" />
<source src="video.webm" type="video/webm" />
Your browser doesn’t support videos natively.
</video>
In this case, the browser that supports HTML5 <video> will first try to load the mp4 video. If it doesn’t
support this format, it will fall back to the webm encoded video. Should that
format not be supported either or should the browser not support <video> at all, it will fall back to the text.
Instead of this text of course, it would make more sense to
fall back to a plug-in based video player in case the browser does not support
HTML5 <video> at all. The following sample uses a
Silverlight video player for this:
<video>
<source src="video.mp4" type='video/mp4' />
<source src="video.webm" type='video/webm' />
<object type="application/x-silverlight-2">
<param name="source" value="http://url/player.xap">
<param name="initParams" value="m=http://url/video.mp4">
</object>
Download the video <a href="video.mp4">here</a>.
</video>
A very similar logic works in CSS as well. In CSS, any
unrecognized properties are simply ignored. So when you want to add multiple,
experimental, vendor-prefixed properties as shown with "border-radius" below, you
can simply include all the variations in your code. This may feel a little
imprecise, but it is easy to use and gets the job done for cases like this.
<style type="text/css">
.target
{
border-radius: 20px;
-moz-border-radius: 20px;
-webkit-border-radius: 20px;
}
</style>
A big upside to feature detection is that it works with
browsers you didn’t think of when creating your page. And since it doesn’t
rely on any browser-based assumptions, it works in future versions of browsers
as well.
Developing and Testing Feature Detection
When you’re ready to develop and test feature detection
across many different browsers, you’ll want to check out the F12 Developer
Tools in Internet Explorer 9. You can use it to debug script step by step,
change the browser’s user agent string, and tell Internet Explorer to use the
rendering engine of previous versions.
Using breakpoints while debugging JavaScript in Internet Explorer 9.
Running in "Document Mode: IE9 standards," the browser uses the modern
"addEventListener" method.
Running in "Document Mode: IE7 standards," the debugger falls back to the
legacy "attachEvent" method.
Change Internet Explorer’s user agent string on the fly using the developer
tools and even add your own ones, including mobile browsers.
Managing Feature Detection in large projects
When creating a complex web project, creating and managing
all the feature detection code yourself can be tedious.
Luckily, there are great JavaScript libraries available that
help in this effort, namely Modernizr and jQuery.
Modernizr already has built-in detection for most HTML5 and
CSS3 features that are very easy to use in your code. Modernizr is very widely
adopted and constantly enhanced. Both Modernizr and jQuery are shipped with the
ASP.NET MVC tools.
Take a look at the following code to detect the browser’s
capability to display web fonts:
Without Modernizr |
With Modernizr |
function(){
var
sheet, bool,
head = docHead || docElement,
style = document.createElement("style"),
impl = document.implementation || { hasFeature: function()
{ return false; } };
style.type = 'text/css';
head.insertBefore(style, head.firstChild);
sheet = style.sheet || style.styleSheet;
var supportAtRule = impl.hasFeature('CSS2', '') ?
function(rule) {
if (!(sheet && rule)) return false;
var result = false;
try {
sheet.insertRule(rule, 0);
result = (/src/i).test(sheet.cssRules[0].cssText);
sheet.deleteRule(sheet.cssRules.length - 1);
} catch(e) { }
return result;
} :
function(rule) {
if (!(sheet && rule)) return false;
sheet.cssText = rule;
return sheet.cssText.length !== 0 &&
(/src/i).test(sheet.cssText) &&
sheet.cssText
.replace(/\r+|\n+/g, '')
.indexOf(rule.split(' ')[0]) === 0;
};
bool = supportAtRule('@font-face { font-family: "font";
src: url(data:,); }');
head.removeChild(style);
return bool;
};
|
<script type="text/javascript" src"modernizr.custom.89997.js"></script>
<script type="text/javascript">
if (Modernizr.fontface){
// font-face is supported
}
</script>
|
"Help!" What to do when a browser doesn’t support the feature you need
If the browser tested doesn’t support a feature you need, feature
detection won’t take the burden of coming up with a workaround from you. Sorry.
In the HTML5 video sample above, using Silverlight as a
fallback was an obvious solution. But what about other HTML5 features like
<canvas> or the new semantic tags in HTML5 including <nav>,
<section> and <article>, <aside> or the new <header>
and <footer>?
There are a growing number of ready-made "fallbacks" for most
HTML5 features, called shims and polyfills. Shims and polyfills are CSS and
JavaScript libraries (or sometimes Flash or Silverlight controls) that you can
add to your project, which will then add in the missing, unsupported HTML5
features.
The general idea is that developers should be able to
develop with the HTML5 APIs, and scripts can create the methods and objects
that should exist. Developing in this future-proof way means as users
upgrade, your code doesn't have to change—users will seamlessly move to the
better, native experience.
The difference between shims and polyfills is that shims
only mimic the feature but has its own, proprietary API. Polyfills
emulate both the feature and the exact API of the HTML5 feature it substitutes.
So generally speaking, using a polyfill saves you the hassle of having to adopt
a proprietary API.
The HTML5
Cross Browser Polyfills collection on github contains a growing list of
available shims and polyfills.
Modernizr, for example, includes the "HTML5Shim" for
semantic tag support, but it is easy to load other shims/polyfills if Modernizr
detects that a feature is not supported.
"Won’t adding all these script libraries make my page huge and slow to
load?"
Glad you asked! It’s true that adding many of these
supporting libraries can add substantial overhead to your site. That’s why you
shouldn’t go overboard—load them dynamically when required. With a shim or
polyfill, the best practice is to only load them when you have detected that
the browser really needs it because the native HTML5 feature is not supported.
Again, you’re in luck! There is a great library that
supports exactly this scenario: yepnope.js
It’s an asynchronous "resource loader" that works
with both JavaScript and CSS and fully decouples preloading from execution. This
means that you have full control of when your resource is executed, and you can
change that order on the fly. Yepnope will be integrated in Modernizr 2, but
can also be used on its own.
Let’s have a look at yepnope’s syntax:
<script type="text/javascript" src="yepnope.1.0.2-min.js"></script>
<script type="text/javascript">
yepnope({
test : Modernizr.geolocation,
yep : nope : [ });
</script>
This example will test the browser’s capability of using
HTML5 geolocation using Modernizr. If supported, your own code (normal.js) will
be loaded, otherwise a custom polyfill (that consists of polyfill.js and
wrapper.js) will be loaded.
Summary: Dos and Don’ts
Let’s finish by summarizing the most important points:
|
DO |
DON’T |
Browser Detection |
Try to avoid altogether
OR
Test for a specific browser AND
version |
Make assumptions for future browsers
by only testing for the browser name. |
Feature Detection |
Test for standards first |
Assume unrelated features under one test. |
Learn more about browser and feature detection at: