Wednesday, May 02, 2007

CSS: CSS Browser Selector


Each browser has its quirks. Having a clear way of organizing the work arounds
for those quirks is a challenge. Using CSS selectors is not a new idea, but I thought it might be helpful to others to give an example of the technique and how we've been able to successfully deploy it for revolutionhealth.com.



First, for IE (the browser that usually requires a hack), we can rely on conditional comments. This is good because it means we don't need to depend on Javascript. For other browsers we'll have to rely on a document.write solution. For Safari, Opera, and Firefox, we rely on the script from http://rafael.adm.br/css_browser_selector/ and for IE, conditional comments.


Here's what we include at the top of our document. (The browser_detect_start partial.)



<!--[if lt IE 7.]>
<div class='ie ie6'>
<![endif]-->
<!--[if IE 7]>
<div class='ie ie7'>
<![endif]-->
<script type="text/javascript">//<![CDATA[
var d = browserCSSDetection();
if( d.browser != "ie" ){ document.write( "<div class='" + d.browser + " " + d.os + "'>" ); }
//]]></script>

And here's what we do for the end of the document. (The browser_detect_end partial.)

<!--[if IE ]>
</div>
<![endif]-->
<script type="text/javascript">//<![CDATA[
var d = browserCSSDetection();
if( d.browser != "ie" ){ document.write( "</div>" ); }
//]]></script>


The browser detection in Javascript. This could be enhanced further, but for us this allowed us to get the site working relatively easy in Konqueror. As well it enabled us to fix our menu's so that they float over flash in Linux using this techinque.



function browserCSSDetection()
{
// see: http://rafael.adm.br/css_browser_selector/
var ua = navigator.userAgent.toLowerCase();
var is = function(t){ return ua.indexOf(t) != -1; };
var b = (!(/opera|webtv/i.test(ua))&&/msie (\d)/.test(ua)) ?
('ie ie'+RegExp.$1) :
is('gecko/') ? 'gecko' :
is('opera/9') ? 'opera opera9' :
/opera (\d)/.test(ua) ? 'opera opera'+RegExp.$1 :
is('konqueror')?'konqueror' :
is('applewebkit/') ? 'webkit safari':
is('mozilla/')?'gecko':'';
// see: http://www.mozilla.org/docs/web-developer/sniffer/browser_type.html
var os = (is('x11')||is('linux'))?' linux':is('mac')?' mac':is('win')?' win':'';
var css = {browser:b,os:os};
return css;
}


Finally, to make all this fit nicely into a layout:

<head>
<%= javascript_include_tag 'browser_detect' %>
</head>
<body>
<%= render :partial => "browser_detect_start" %>
<%= @content_for_layout %>
<%= render :partial => "browser_detect_end" %>
</body>


Update:
Here's a simple example of what this enables. The advantage of this over a conditionally included file is it keeps everything about the login box isolated to one place. You don't need to worry about openning up a separate file to make your IE fixes. We do use iefix specific CSS for our site, but only for very large features like menus



#login {
padding:0px;
}
.ie6 #login {
padding: 2px;
}

7 comments:

Anonymous said...

i appreciate what youre trying to do here but ive got to say YUK... gone are the days of horrible document.writes and inline javascript like this and i dont even want to go near accessibility when half your tags are inserted in js.

Anonymous said...

Anon:

It's not "half the tags"; it's a single DIV tag that wraps the content.

View the site with JS turned off, and you'll see it degrades very gracefully.

Paul said...

my mistake todd, i was just taken aback by the document.writes i didnt read it all properly.

can i suggest something tho that might work for you? theres a few options and you may have tested them already, i havent but would be interested to see them working.

if you did this:
!html
!head
!style type="text/css"
.safari h1 {color:blue;}
!/style
!script type="text/javascript"

function browserDetect() {
// see: http://rafael.adm.br/css_browser_selector/
var ua = navigator.userAgent.toLowerCase();
var is = function(t){ return ua.indexOf(t) != -1; };
var b = (!(/opera|webtv/i.test(ua))&&/msie (\d)/.test(ua)) ?
('ie ie'+RegExp.$1) :
is('gecko/') ? 'gecko' :
is('opera/9') ? 'opera opera9' :
/opera (\d)/.test(ua) ? 'opera opera'+RegExp.$1 :
is('konqueror')?'konqueror' :
is('applewebkit/') ? 'webkit safari':
is('mozilla/')?'gecko':'';
// see: http://www.mozilla.org/docs/web-developer/sniffer/browser_type.html
var os = (is('x11')||is('linux'))?' linux':is('mac')?' mac':is('win')?' win':'';
return {browser:b,os:os};
}

with (browserDetect())
document.getElementsByTagName('html')[0].className += ' ' + browser + ' ' + os;

!/script
!/head
!body
!h1 hello !/h1
!/body
!/html

You dont need anything in the body. If anything it probably doesnt work with old browsers, whats compatibility are you aiming for?

(obviously you need to add the tags back in

Mr eel said...

Nested ternary operators! I appreciate what you're doing here, but that's some seriously nasty looking Javascript.

todd said...

Paul,
The main reason I switched to document.write instead of a DOM based solution is because of how the browser renders the page when you use a pure DOM solution, you get a double render affect, and safari does not always rerender the document correctly with all the new CSS rules. If you imagine the document streaming through the browser then document.write is a very useful tool to leavage anytime you need something in place before the browser reveals the page to the user. The browser will block rendering until document.write completes, meaning it's a guaranteed thing that the DOM will be fully constructed and each CSS rule can apply on the first pass. I wouldn't close the door on a purely DOM based solution. Reading YUI library the onAvailable method might be interesting to use as an alternate approach.

todd said...

Mr. Eel,

browserCSSDetection, is not my own Javascript it's the work of http://rafael.adm.br/css_browser_selector/ as I stated in the post... I did make a few minor modifications to suite our needs. What is nice about it, is it being a compact, working, tested solution for browser detection. I agree it's not the nicest piece of code, but then we are talking about browser detection here :-)

Paul said...

i hear what youre saying and therefore why you did it. just thinking like a browser for a second tho. if i include the bit of code that i have about before the css includes

<%= javascript_include_tag 'browser_css' %>
<%= stylesheet_include_tag 'mycss' %>

Then i first load the javascript which adds the classes to the html element of the page. Then i load the css which styles the page according to the classes.

This way you require no js in the page body and theres no double render. get what i mean?