No more CSS hacks: Browser sniffing with .htaccess
- Categories:
- CSS, PHP
- Tags:
- browser, css, detection, hacks, htaccess, html5, php, sniffing
- Published:
- 2:40pm on Sunday 15th May, 2011
Ask any web developer what the worst part of their job is, and chances are that browser testing will feature pretty high in the list. Ever since we ditched HTML tables and started using CSS to lay out our web pages, it has been a constant struggle to make designs look the same in every browser.
Of course, for the biggest headache Internet Explorer there has always been the blessing of Conditional Comments. These specially crafted comment-like tags let us specify IE-specific styles or stylesheets, safe in the knowledge that they will only be recognised and downloaded by the targeted browser(s). And if it was only IE that tripped us up, that would be fine. But inevitably once you start building bigger and more complex designs, you are going to run across niggling little inconsistencies in one browser or another where Conditional Comments can’t help you out.
Previously the only way to deliver different CSS rules for browsers was to rely on a complicated set of ‘hacks’—either parsing bugs or unsupported CSS3 declarations that allowed some rules to be ignored by some browsers. But it’s not easy to remember every convoluted hack, and when you’re up against a deadline and Opera 9 doesn’t render quite the same as Opera 10 you have better things to do than Google for that specific fix.
Luckily, there is an easier way to detect browsers and their versions.
Special (User) Agent
When any browser requests a web page from your server, it sends a bunch of request headers. One of those headers tells the web server what the browser is called: the User-Agent string. There are approximately a gazillion of these strings—have a look at Zytrax.com’s list if you’re interested in learning some of the more bizarre options—but for the purposes of modern web development we’re really only interested in the most commonly used browsers:
- Microsoft Internet Explorer
- Firefox
- Safari
- Opera
- Google Chrome
Unless you’re writing a site specifically for obscure Linux distros, these five browsers will probably make up 99% or more of your visitors. Each browser manufacturer uses a slightly different string—some even include other browser’s names in their user-agent—but they (generally) tend to follow a predictable pattern. And that means we can recognise and extract them using regular expressions.
Regular expressions and .htaccess
If you’re used to running on Apache you probably have some experience with the .htaccess file. It is used to control access to files and folders, redirect requests, and provide various server-level bits of information back to the browser. But it can also be used to create additional environment variables that you can use in your web pages.
What we’re going to do is interrogate the User-Agent string for our five browsers, and extract the browser name and version numberfor use in our pages. First, add these lines to your .htaccess file:
What we’re doing here is using a regular expression to look for the browser name, followed by a space or a forward-slash, and then a number. The order of browser names is important here, as Chrome’s User-Agent string includes “Safari” (and Camino includes “Firefox,” if you want to add that browser into the mix). You might also notice that Safari is missing; in its place is a mysterious browser called “Version.” That’s due to how Safari UA strings are constructed; they use a build number at the end instead of a version number, with the version number earlier in the string. Using regular expression backreferences we assign the first matched expression to the variable ‘browser’ and the second match (a single number) to the variable ‘version.’ The second line accommodates Safari’s difference by checking for the not-really-a-browser “Version” and replacing it with the correct name. And finally we trim Internet Explorer’s name down to the more familiar “IE.”
So now we have grabbed a couple of useful bits of information out of the User-Agent string of the user’s browser—what can we do with them?
Environment variables in PHP
Any variables created in this way are added to PHP’s superglobal array $_ENV. Just like the $_GET and $_POST arrays, you can output the values by referring to the array key:
<p>You are using <?php echo $_ENV[‘browser’]; ?>, version <?php echo $_ENV[‘version’]; ?></p>
Environment variables also have a second access method, the getenv() function. Simply pass the variable name in the function call to retrieve the value:
<p>You are using <?php echo getenv(‘browser’); ?>, version <?php echo getenv(‘version’); ?></p>
Very nice, but how can we use this information instead of CSS hacks? Simple; we just add the browser and version to the class of the <html> element:
<html class=”<?php echo getenv(‘browser’) . getenv(‘version’); ?>”>
This technique allows you to write browser-specific rules that override the base declaration by including the browser-specific class name:
#mydiv {
position: fixed;
}
.IE6 #mydiv {
position: absolute;
}
Arguably this approach makes more sense than conditional comments, as it means that targeted browsers aren’t required to download an additional file, but you should probably look at your site stats to see whether your Internet Explorer traffic is high enough to make up for other browsers taking the hit of downloading unused styles.
Adding classes to the <html> element is not valid HTML in HTML4 or XHTML1, but it is valid in HTML5. Tools like Modernizr have adopted this technique when augmenting markup, and I think it is a sensible place to put these new classes (particularly as IE6 has problems with multiple classes in declarations, so putting them on the <body> might have unforeseen consequences).
But I’m not allowed to edit my .htaccess!
As it’s a simple regular expression, the functionality here can be duplicated in PHP alone very simply. Just place this code before the start of markup (or in a global include file if you’re using a CMS):
Then simply refer to those new variables instead of the environment vars:
<html class=”<?php echo $browser . $version; ?>”>
Justification
“Hang on just a gosh-darned minute,” I hear you say, “Browser sniffing is bad!”
Well, yes it is, in the context of late ‘90s JavaScript, but I’m of the opinion that pragmatism trumps idealism. In a perfect world, every line of CSS we write would be a well planned part of an overall framework, and every element on the page would be carefully constructed to avoid any possible browser inconsistencies, but anyone working on the web today knows that is rarely the case. Budgets and timeframes aren’t designed to accommodate more than a cursory glance at older browsers, and sometimes a quick fix is all that is needed; it’s in these cases that being able to target a specific combination of browser and version comes in very handy.
That said, I would not advocate using this technique to target the most up-to-date versions of modern browsers. If you’re having to hack the current version of a browser then you’ve done something wrong, and run the risk of that same thing breaking as soon as Firefox 5 or IE10 is released. Far better to ‘fix’ older, static versions of browsers—Internet Explorer 7 and below, Opera 9 and lower, Firefox 3 and so on—as you know they are not going to change their behaviour.
Example page
Here’s an example page showing the technique in action. Note that it could also be extended to detect the platform (Windows or Mac) of the user as well, in case you want to get even more specific. You might want to make use of this technique to show or hide the right buttons to download software, for example, by writing something like this:
.download-win, .download-mac {
display: block;
}
.Mac .download-win, .Win .download-mac {
display: none;
}
Note that, depending on the policy of your webhost, you might have to follow certain restrictions when it comes to naming your new environment variables. For example, MediaTemple insists that any variables created in this way must start with “HTTP_”.
If you have any more suggestions for how to extend or improve the idea, let me know in the comments.

I'd love to hear what you think - please use the form below to leave your comments. Anything I consider too offensive, off-topic, or spammy will be deleted at my discretion. Some HTML is permitted, or you can use Textile.
Pete Lambert at 9:14am on 16th May, 2011 #
James John Malcolm at 9:50am on 16th May, 2011 #
Alex Jones (@BaldMan) at 2:03pm on 16th May, 2011 #
Lammert Postma at 1:41pm on 19th May, 2011 #
Matthew Pennell at 8:36am on 21st May, 2011 #
Lammert Postma at 10:36pm on 21st May, 2011 #
Matthew Pennell at 8:49am on 22nd May, 2011 #
Stephane Deschamps at 12:08pm on 10th June, 2011 #