Stylesheet Sanity, Part 2
Published 11 December 05 by Justin French, 12 comments
Before we get started, I’m not talking about the visual layering of objects on the screen (z-index), nor am I talking about Modular CSS, which is a related topic with different goals.
The idea behind Layered Stylesheets is that you start with a stylesheet that contains your most basic, generic, non-specific, reusable rules (the things you find yourself doing over and over again), then layer on additional stylesheets which do things more specific to a particular site, a particular section of the site, or even a specific page.
Each additional layer of CSS rules will first inherit from those already set, then build upon and override them.
Everyone Likes an Example
I’ve been using this on many sites, but here’s some (trimmed down) source code direct from the new TextDrive home page we launched a few days back:
<link href="/stylesheets/base.css" ... />
<link href="/stylesheets/default.css" ... />
<link href="/stylesheets/home.css" ... />
... and the same from the contact page:
<link href="/stylesheets/base.css" ... />
<link href="/stylesheets/default.css" ... />
<link href="/stylesheets/contact.css" ... />
... and the same from the hosting section:
<link href="/stylesheets/base.css" ... />
<link href="/stylesheets/default.css" ... />
<link href="/stylesheets/hosting.css" ... />
No doubt you’re seeing a pattern. The first layer loaded (base.css) is a work in progress to say the least, but it contains some that I personally like to start with as a foundation — setting margin-top:0; on almost everything, setting the border to none on images inside <a> tags, etc. They’re personal preferences of mine that you’ll see on almost everything I build.
The second layer (default.css) sets the scene for the entire website, building upon the rules already set in base.css. It’s here that I set colors, declare most of the typography, style the site-wide navigation, set-up the layout and columns — anything that all pages (or at least most of them) are likely to need goes here.
The third layer (home.css, contact.css or hosting.css) is only loaded depending on which section of the site we’re currently viewing. It contains all the rules and styles specifically required for a certain section or page.
Why?
Much like the techniques discussed in part 1, this allows me to address particular elements in certain sections of the site without applying a whole bunch of classes and ids to everything. Both my HTML and CSS files are a lot less verbose.
I can write rules for the home page without worrying about how those rules affect other pages, I can re-use base.css on many many sites (saving me hours of work), I don’t have to scroll through hundreds of lines to find a particular rule, co-workers know exactly where to place styles for a particular section they’re working on and they can easily take a guess at where they’ll find things they need to tweak.
For a small site, the concepts in part 1 may be more than enough, but for larger sites with many sections, or for a team of developers collaborating on a large web application, I’d sya this is almost essential.
But There’s More!
Perhaps the most interesting thing about all this is that just after we launched the new TextDrive site, I was faced with the task of a quick on-the-fly redesign of the weblog, knowledge base (both Textpattern installs) and a few other related sites.
By separating the more generic CSS rules from those that are very specific to particular pages of the site, I was able to instantly reuse base.css and default.css elsewhere. In a few short minutes, I had redesigned a handful of websites. For lack of a better term, the combination of base.css and default.css became a resuable identity for TextDrive.
Sure, I still have some work to do to refine these related sites (which will be done in additional layers), but there’s no way I could have pulled this off given a single file with hundreds of very specific rules mixed together.
Some Rails Goodies
Rails is our platform of choice, so naturally I have some ideas and code to share.
The obvious choice here is to name the files for our third layer after the controllers in our application.
- HomeController => /public/stylesheets/home.css
- ContactController => /public/stylesheets/contact.css
- Admin::DashboardController => /public/stylesheets/admin/dashboard.css
So here’s a quick helper method we could add to app/helpers/application_helper.rb and call from our layouts:
def stylesheet_tag_for_controller()
uri = "/stylesheets/#{@params[:controller]}.css"
"<link href='#{uri}' rel='stylesheet' media='screen' type='text/css' />"
end
But there’s bound to be plenty of controllers that don’t need their own stylesheet, so it’d be nice if the helper checked the filesystem to make sure the file existed. Easy:
def stylesheet_tag_for_controller()
uri = "/stylesheets/#{@params[:controller]}.css"
file = "#{RAILS_ROOT}/public/#{uri}"
if FileTest.exists?(file)
"<link href='#{uri}' rel='stylesheet' media='screen' type='text/css' />"
end
end
Before you go copying and pasting, I have a few more tweaks. I’ve discussed this concept a few times on this site already, but I find it’s useful to append the modified time of the file onto the end of the stylesheet URI as a query string. This basically creates a new URI for each revision of the stylesheet, which means most browsers will be forced to use the new version, rather than some older cached version.
Since it’d be nice to do this for all my stylesheet link tags, I’ll change the name to stylesheet_tag(), and make it much more useful with some html_options and sensible defaults, then let stylesheet_for_controller() build on it:
def stylesheet_tag(name, html_options = {})
uri = "/stylesheets/#{name}.css"
file = "#{RAILS_ROOT}/public/#{uri}"
if FileTest.exists?(file)
modified = File.stat(file).mtime.strftime("%Y%m%d%H%M%S")
html_options = {
'href' => "#{uri}?v=#{modified}",
'type' => "text/css",
'media' => "screen, projection",
'rel' => "stylesheet"
}.merge(html_options.stringify_keys)
tag("link", html_options)
end
end
Then my layout simply calls the stylesheets in the order they need to be layered:
<head>
<%= stylesheet_tag "base" <span>>
<</span>= stylesheet_tag "default" <span>>
<</span>= stylesheet_tag @params[:controller] %>
</head>
Which will render something like:
<head>
<link href="/stylesheets/base.css?v=20051128022846" media="screen, projection" rel="stylesheet" type="text/css" />
<link href="/stylesheets/default.css?v=20051124075621" media="screen, projection" rel="stylesheet" type="text/css" />
<link href="/stylesheets/home.css?v=20051128133231" media="screen, projection" rel="stylesheet" type="text/css" />
</head>
The <link> tags are only generated if the file exists (so there’s no 404s), the modifed date and time is appended to the URI automatically to ensure that browser don’t cache old versions of the files, and our preferred default attributes for the <link> tag are built into the helper method, so we don’t have to repeat ourselves.
You can obviously extend this layering a lot further with print and aural stylesheets – maybe even a layer inbetween base and default which addresses not-so-modern browsers. Use this technique on it’s own as I have or blend it with Modular CSS, @import rules, CSS hacks, IE conditional comments, and anything else you like.
Not Exactly Ground-Breaking
True. There’s nothing particularly revolutionary about Layered Stylesheets, but when I see most developers slugging it out with a single CSS file containing well over a thousand lines, I feel compelled to share my way of doing things, in the hope that others will find also find it useful.
Enjoy.
Update: I’ve turned comments off for this article. You may thanks the spammers.