[Skip top navbar]

Andrew Gregory's Web Pages

Rocky outcrop, 28°30'11"S 122°46'31"E

-

User Agent CSS

(Comments may be emailed to me or left at my blog entry for this)


Background

I recently came within a hair's breadth of needing to implement some sort of browser sniffing on my web site in order to work around some Safari/Konqueror CSS bugs. While I eventually managed to get away without browser sniffing, it took hours of experimentation with various arrangements of rules to work out. By the end of it I was wishing there was some way to deliver specific CSS rules to specific browsers that could be done in seconds instead of hours.

My solution was to come up with a Perl script that filters style sheets looking for special markers that indicate a CSS property is for a particular browser. I modelled my syntax from the Vendor-specific extensions in the CSS 2.1 spec, and ensured it followed the existing IDENT syntax. I didn't see much point using a non-standard syntax unnecessarily.

Example:

foo { font-family: sans-serif; content: "Unrecognized browser"; -uacss-awk-content: "AppleWebKit (Safari/OmniWeb)"; -uacss-konq-content: "Konqueror"; -uacss-gecko-content: "Gecko"; -uacss-opera-content: "Opera"; -uacss-iewin-content: "IE/Windows"; -uacss-iemac-content: "IE/Mac"; color: magenta; background-color: white; }

So long as all instances of the same property are grouped, the script will only output one instance of the relevant property, discarding the others. For example, given the sample above a Gecko browser would receive:

foo { font-family: sans-serif; content: "Gecko"; color: magenta; background-color: white; }

If the user agent was not recognized by the script then all recognized user-agent specific rules are removed, and that user agent would receive:

foo { font-family: sans-serif; content: "Unrecognized browser"; color: magenta; background-color: white; }

Any unrecognized user-agent specific rule is left in the output, in case it is something like "-moz-border-radius".

If the W3C CSS Validator is detected, then all vendor-specific rules are removed from the output.

Caching

The immediate concern with web requests that return different data depending on the User Agent is when data for one User Agent is cached and sent to a different User Agent. My script handles that by sending a Vary header that indicates to anything listening that the response will vary depending on the User Agent header. Software performing caching then has a choice about how to handle that, either by not caching or by caching multiple copies as different user agents request the data. If you are in the unfortunate situation where some software between the browser and the server doesn't correctly handle the Vary header, then this script will most likely be unusable.

Apart from ensuring the correct data gets to the browser, there is also the efficiency aspect of caching. To handle that my script outputs a Last-modified header that reflects the modification date/time of the original CSS file. Testing using my own local Apache server, and my ISPs Apache server found that that was all that was needed to have the server return complete "200 OK" or much shorter "304 Not modified" responses as appropriate.


Usage

Installation and Setup

Grab the script:

It's written in Perl, so you must be able to run Perl scripts on your web server. Install it somewhere on your web server. Don't forget to mark it executable.

The next step is to get it to process your style sheets. There are two methods that have worked for me:

.htaccess

If your web server supports .htaccess, adding the following to a .htaccess file will cause all CSS files in the same folder and sub-folders to be processed by my script:

Action text/css /virtual/path/to/uacss.pl

Example:

Action text/css /cgi-bin/uacss.pl

The benefit of this techique is that no changes need to be made to any of your web pages that reference the style sheets.

Path Translation

This requires every file that references the style sheets to be modified:

/virtual/path/to/uacss.pl/virtual/path/to/styles.css

Example:

/cgi-bin/uacss.pl/styles/basic.css

Example HTML:

<link href="/cgi-bin/uacss.pl/styles/basic.css" rel="stylesheet" type="text/css">

CSS User Agent Syntax

To use the script to deliver specific CSS properties to specific user agents, you need to prefix the property name with "-uacss-" (this requirement can be removed - see the script), followed by the user agent code, followed by an optional version comparison sequence (see below), followed by a "-" then the property. For example:

foo { content: "This goes to unrecognized browsers"; -uacss-awk-content: "This goes to AppleWebKit browsers"; -uacss-iewin_ge_6_0-content: "This goes to IE/Windows version 6.0 or later"; -uacss-iewin_lt_6_0-content: "This goes to IE/Windows before version 6.0"; -uacss-konq_gt_3_1_le_3_3-content: "This goes to Konqueror browsers later than version 3.1 (but not equal to 3.1) AND less than or equal to version 3.3"; }

Multiple version comparisons on the same property are AND-ed together and must all be true for the property to be used. OR-ing of conditions is done by multiple lines, although only the last property value will be used.

Note that for the last example, version 3.1.1 is not considered "greater than" version 3.1. If version 3.1.1 was to be included in that final example line then the test should have been "gt_3_1_0".

Parameters

The script supports setting the user agent code and version via the query string. It can also be set to output the original CSS without modification ("passthru"). "ua" and "ver" specify the user agent code and version respectively. Examples:

/styles/basic.css?passthru /styles/basic.css?ua=w3c /styles/basic.css?ua=iewin&ver=6_0 /cgi-bin/uacss.pl/styles/basic.css?ua=gecko


Good Aspects Of This Script

Fine Grained Control

You're able to give different browsers different CSS properties on a per-property basis.

Related Properties Are Kept Together

Other ways of giving different browsers different properties typically end up requiring separate files for each browser, with the resultant necessary duplication of selectors and the possibility of things getting out of sync.

Much Easier To Read

The property prefixes are self-documenting. Documentation is always a problem - it gets forgotten or becomes stale. A regular question on the css-d mailing list is from people wondering what a bit of CSS code with various hacks is doing. With this script it is obvious to the maintainer which browsers get which property.

No Reliance On Browser Bugs

The vast majority of CSS "hacks" revolve around the use of CSS parser bugs. Relying on bugs for the correct operation of your software is just asking for trouble because there is no relationship between parser bugs and CSS bugs.

It's already happening. Microsoft are fixing various CSS parser bugs, but cannot fix all the CSS issues that are being worked around through the use of those bugs. (Doing that would require Microsoft to develop a bug-free browser, which is a totally unrealistic expectation of any browser developer.)

It's Designed To Be Ignored

This is good! By design all your user agent specific properties will be ignored by browsers if they somehow get the unprocessed stylesheet (some sort of server failure).

Your Style Sheets Validate

When the script detects the W3C CSS validator, it strips out all vendor specific properties. Apart from the fact they can't be validated anyway, they can clog up the validation report with things you know are wrong possibily distracting you from any real CSS issues you need to deal with.

No JavaScript Reliance

No need to worry if your visitors have disabled JavaScript!

Bad Aspects Of This Script

You Need A Server...

It is a server-side script after all, and it means you can't test locally unless you have a server. Without a server you should expect all the "-uacss" lines to be ignored by your browser, i.e. you won't see any of the results of your browser-specific properties.

...With Perl Support

The script is written in Perl. Just standard Perl, no fancy modules required. Perl shouldn't be a big problem. Most web hosts support Perl.

It's Browser Sniffing

Browser sniffing is generally considered a bad idea. However, I think that's mostly because so many people do such a bad job of it! Besides, a server-side script has no other option when it comes to working out which browser is visiting.

As a user of the Opera browser, and as a web developer, I've seen so many bad attempts at browser sniffing that I'd like to think that I've learnt from what I've seen and that I'm not repeating previous mistakes.

Ideally, I would have liked my script to detect rendering engines, for example "tasman" instead of "iemac". The problem there is that the script would then need to be programmed with all the relationships between user agent strings and rendering engines. That would be time consuming, error-prone, and require regular script updates. By just extracting the basic information available in the user agent string, the script is kept simple, more reliable, and more transparent.

I should point out that a small number of people use anonymizing software that scrubs out or junks the user agent string. Those people will be treated as unrecognized browsers.

Future Maintenance Is Required

This is not unique to this script. Every time you introduce a browser dependancy you are also guaranteeing the need to come back and maintain things occasionally. However, the browser specific properties of this script are distinctive and easy to search for ("-uacss-") making maintenance a lot quicker than searching for elusive hacks.

The Script Requires One Property Per Line

To keep the script simple, the script processes the CSS file on a line-by-line basis. While you can have multiple properties per line, the script only considers the first property on each line.


Guidelines

Only fix what you know to be broken.

That's probably the most important point - it's also where most browser sniffing breaks down. A problem I still see regularly on the 'net is browser sniffing targeting Opera. Any version of Opera. The script may have been written in 2000 when every current version of Opera had DHTML problems, but those problems disappeared more than three years ago.

It is very important you only fix issues that you know exist. As you will never know for certain what problems future browsers may or may not have, you must assume all future browsers will be 100% correct in their operation. In practice, that means:

You should be at least using "lt/lte" version comparison operators in all your "-uacss-" properties.

A consequence of that is that you will have to return to your style sheet in the future and increment version numbers as new browsers are released.

If you have a browser specific property, you should always have the standard version too.

What that means is that if you need to send, for example, a particular width value to a browser, you should also have the standard width property present too:

padding: 10px; width: 50px; /* standard width */ -uacss-iewin_lt_6-width: 70px; /* UA specific width */

It's probably not obvious, but by definition the browser-specific properties will all be non-standard. For that reason it's very important to ensure you have a standard property (the plain width in the above example) with a value for use by standards-compliant browsers. You should be coding as if some future 100% standards-compliant browser would be able to take the "passthru" or "ua=w3c" output and render exactly as you intended.


Interesting Links


-