BTreeHugger's Beat

Safe modern CSS Hacks

After collecting some fairly comprehensive test results across many browser versions, I've managed to find relatively "safe" (future proof and consistent) CSS hacks to target specific modern browsers. They're based on concepts from earlier CSS hacks I've been using for many years which haven't broken yet, or required little effort to fix, and have been tested in dozens of browsers.

You can skip straight to the hacks if you already know how to use them. You can also try the tests in your browser on the test page.

Safe? Are you sure?

There are a number of reasons that these hacks could fail, and some of them are subtle. However, the good news is that they are chosen to be as future-proof as possible. New browsers may come around that also apply the hacks, but it's very unlikely as they're just complex enough to target only specific browsers with specific sets of quirks.

Since these hacks are actually exploiting the corner-cases of very specific properties, it's quite likely that they'll be safe. In fact, I've used similar hacks in the past, but had far less data to create them with.. as a result, some of them were so general they are now applied by modern browsers. To prevent this, I've created a grid of specific CSS clauses to see what browsers apply them (as part of my browser-testing project).

This grid can be seen here, if you'd like to create your own hacks. Note that it's not exhaustive, as older versions of these browsers do not automate my tests properly, or I simply don't have access to them.

How do they work?

Simply put, they are combinations of the clauses in the results in the test grid. Most of them are simply a combination of html and body pseudoclass/attribute hacks, although sometimes case-sensitivity comes into play. In some cases, media queries are necessary to filter things down so that, for instance, Konqueror won't apply a hack that only Safari/Chrome ought to apply (or vice-versa). Notably, Firefox 3.5 and up require a media query to distinguish them from modern WebKit browsers.

Using a hack is pretty straightforward. Just take the CSS rule you only want applied by a particular hack, and dress it up with the hack. For instance, say you have some CSS you'd only like applied in Firefox 3.5 and up for some reason:

.b { font-weight:bold; }
.n { font-style:normal; }

You'd simply prefix the rules appropriately, wrapping them in the media query, like this:

@media (color), not (color) {   html:not([missing=""]):not(:nth-child(+0)) .b { font-weight:bold; }
  html:not([missing=""]):not(:nth-child(+0)) .n { font-style:normal; }
}

Of course, there are some subtleties to bear in mind when using these hacks..

Media query hacks

In some cases I've only been able to target specific browser versions by using in-stylesheet media queries. There are some things to bear in mind when using these hacks. First, the use of media queries for hacking limits their utility for legitimate use, since (confusingly) they cannot be nested. For instance, if you wanted to target only color screens, you couldn't nest a media-query based hack inside of that. That's why I only use media queries where they are necessary to achieve a given hack.

Additionally, some browsers struggle to parse media queries. NetFront under 3.5, for instance, will ignore the rest of the stylesheet after a non-trivial media query. In this case it works to our advantage, since those versions stupidly apply just about any pseudoclass you throw at them, and will thus many of these hacks. Thus, I usually filter out old versions of NetFront where necessary just by adding something like @media (color) {} just above the rest of the stylesheet that ought to be ignored.

However, some browsers just have trouble figuring out where a media query might end, which can cause headaches. As a result, it's usually safer to end your media queries with dummy {} @media {} so these browsers don't get confused. It's probably a good idea in general to use this wherever you follow a media query with additional CSS, if you want to ensure older browsers won't get confused.

Attribute selector hacks

These hacks basically test for the presence of an attribute, or whether it's value satisfies meaningless tests that should always behave the same way regardless of the actual value. It's likely that using a non-standard attribute (like xmlns or dir, depending on your doctype) may result in these hacks not working; I'll confirm this at some later date).

Particular browsers also ignore case-sensitivity on attribute values; but some valid attributes aren't always case-sensitive to begin with, so we must exercise a bit of caution. You may assign an ID or class to the element in question and use that, or use another case-sensitive attribute like xmlns (not dir).

Body-based hacks

My tests were all with a very standard, straightforward file (as you can see on the test page). In other words, any hacks using the body element may fail to work properly if you don't have only a head followed by a body in your top-level html element. Creating hacks for other document skeletons is well beyond the scope of my testing.

Can I roll my own?

Of course! Just combine the appropriate clauses from my test grid as I've obviously done to create these hacks. This may come in handy, for instance, if you want to find a hack to target several browser families and version but filter out the rest. Good hunting!

And now.. the hacks

Firefox (and other Gecko-based browsers)

Separating WebKit from Gecko is getting trickier and trickier, but it's still possible to target Firefox 3.5 and up using media queries. Targeting older versions is straightforward enough.

3.5 up:
@media all and (orientation) { html:not(:last-child) rule }
1.0-2.0.0.20:
html:not([attr*=""]) #xxx[id='XXX'] rule
1.0-3.0.19:
html:not([missing=""]):not(:last-child):not([attr*=""])[attr$=""] rule

Internet Explorer

I would advise using conditional comments instead of these, although using them within a conditional stylesheet may let you get away with having only one such stylesheet. In fact, the IE8 only works in a conditional comment, otherwise it will be applied by a bunch of other browsers.

9 preview 4:
html[missing=""] rule
8:
html[attr$=""] head + /**/ body rule
7:
:first-child+html rule
6 down:
* html rule

Safari and Chrome

Since Safari and Chrome versions share the same WebKit core, there aren't hacks to separate the two. That hardly matters much, but at least you can separate them from Konqueror. Note that I have tried a few other WebKit browsers, but don't have comprehensive test data on them.

Please note that Safari 4.0 and it's beta used slightly different versions of WebKit, hence the weird boundaries for those hacks. Safari 4 Mobile on iPhone OS 3.0 appears to use the same WebKit the beta did, but I have not tested other iPhone versions.

Earlier versions of Safari require a media query to isolate them from Konqueror. But if you don't care, you may drop the media query part and use just the hack within.

Safari 4.0.4 up and Chrome 5.0.396.0 up:
body:not(:nth-last-child(-1n)):nth-child(n0) rule
Safari 4.0.4 up and Chrome 5.0.366.2 up:
html:not(:nth-child(-n1)):not(:nth-child(+0)) rule
Chrome 5.0.366.2-5.0.375.29:
body:nth-last-child(-1n):not(:nth-child(+0)) rule
Safari 3.1-4.0.3 and Chrome 0.2.149.27-5.0.375.29:
body:nth-last-child(-1n):not(:nth-child(-1n)) rule
Safari 3.1-4.0 beta and Chrome 0.2.149.27-2.0.156.1:
body:not(:root:root):not(:nth-child(-1n)) rule
Safari 4 beta up and Chrome 2 up:
html:not([attr$=""]) body:nth-child(n1):not(:nth-child(-1n)) rule
Safari 3.1 up and Chrome:
html:not(:last-child) body:not(:nth-child(-1n)):nth-last-child(n0) rule
Safari 3.1-3.2.3 and Chrome 0.2.149.27-1.0.154.65:
@media all and (min-width:-100px) { html[attr$=""] body:not(:root:root):not(:nth-child(-1n)) rule  }
Safari 3.0-4.0 beta and Chrome up-2.0.156.1:
@media all and (min-width:-100px) { head~body:not(:root:root) rule  }
Safari 3.0-3.2.3 and Chrome up-1.0.154.65:
@media all and (min-width:-100px) { html[attr$=""] head~body:not(:root:root) rule  }
Safari 3.0-3.0.4:
@media all and (min-width:-100px) { head~body:not(:root:root) #xxx[id='XXX'] rule }
Safari 2.0-4.0 beta and Chrome up to 2.0.156.1:
@media all and (min-width:-100px) { body:not(:root:root) rule  }
Safari 2.0-3.2.3 and Chrome up to 1.0.154.65:
@media all and (min-width:-100px) { html[attr$=""] body:not(:root:root) rule  }
Safari 2.0-3.0.4:
@media all and (min-width:-100px) { body:not(:root:root) #xxx[id='XXX'] rule }
Safari 2.0-2.0.4:
html:last-child rule

Opera

The Opera hacks are quite varied, most requiring a media query. Note also that Opera Mini and Mobile more or less behave the same way as particular versions of desktop Opera, so I've found no hacks to distinguish them.

10.6 up:
@media (orientation) { body:nth-child(+n) rule }
10.5 up:
html:not([attr$=""]) body:nth-child(+n):not([missing=""]) rule
10 up:
@media (min-width:0px) { body:nth-child(+n):not([missing=""]) rule }
10-10.5:
*~html body:nth-child(+n):not([missing=""]) rule
10-10.1:
@media not (-webkit-min-device-pixel-ratio:0) { rule }
9.5 up:
body:nth-child(+n):not([missing=""]) rule
9-9.27:
@media all and (min-width:0px) { html[attr$=""]:first-child rule }
7.5-9.27:
@media all and (min-width:0px) { html[attr]:first-child rule }
7.5-8.54:
@media not all and (min-color:0) { html[attr] rule }
7.2-9.27:
@media all and (min-width:0px) { html:first-child rule }
7-7.54u2:
@media not all and (min-color:0) { #xxx[id='XXX'] rule }
7-8.54:
@media not all and (min-color:0) { rule }

Konqueror

Konqueror is surprisingly straightfoward to hack for, because it has it's quirks with what it considers to be the position of the body element. That being said, hacking for 4.1 requires media queries.

4.2 up:
html:not([attr$=""]) body:not(:nth-child(n0)) rule
4.1.2-4.1.3:
@media (min-width:0px) { html:not([attr$=""]) body:nth-child(-1n) rule }
4.1-4.1.3:
@media (min-width:0px) { body:nth-child(-1n) rule }
4.1-4.1.1:
@media (min-width:0px) { html[attr$=""] body:nth-child(-1n) rule }
3.4-4.1.3:
body:nth-child(-1n) rule
3.4-4.0.5:
body:nth-child(-1n):not(:root:root) rule
3.4-3.5.5:
body:nth-child(-1n) #xxx[id='XXX'] rule