Migrating a static 960.gs grid to a responsive, semantic grid with LessCSS
The layout of Antiques Near Me (a startup I co-founded) has long been built using the sturdy 960.gs grid system (implemented in Drupal 6 using the Clean base theme). Grids are very helpful: They allow layouts to be created quickly; they allow elements to be fit into layouts easily; they keep dimensions consistent; they look clean. But they have a major drawback that always bothered me: the grid-X
classes that determine an element's width are in the HTML. That mixes up markup/content and layout/style, which should ideally be completely separated between the HTML and CSS.
The rigidity of an in-markup grid becomes especially apparent when trying to implement "responsive" design principles. I'm not a designer, but the basic idea of responsive design for the web, as I understand it, is that a site's layout should adapt automagically to the device it's viewed in. For a nice mobile experience, for example, rather than create a separate mobile site - which I always thought was a poor use of resources, duplicating the content-generating backend - the same HTML can be used with @media
queries in the CSS to make the layout look "native".
(I've put together some useful links on Responsive Design and @media queries using Delicious. The best implementation of a responsive layout that I've seen is on the site of FourKitchens.)
Besides the 960 grid, I was using LessCSS to generate my styles: it supports variables, mix-ins, nested styles, etc; it generally makes stylesheet coding much more intuitive. So for a while the thought simmered, why not move the static 960 grid into Less (using mixins), and apply the equivalent of grid-X
classes directly in the CSS? Then I read this article in Smashing on The Semantic Grid System, which prescribed pretty much the same thing - using Less with a library called Semantic.gs - and I realized it was time to actually make it happen.
To make the transition, I forked semantic.gs and made some modifications: I added .alpha and .omega mixins (to cancel out side margins); for nested styles, I ditched semantic.gs's .row()
approach (which seems to be buggy anyway) and created a .nested-column
mixin instead. I added <span class="kw1">clear</span><span class="sy0">:</span><span class="kw2">both</span>
to the .clearfix
mixin (seemed to make sense, though maybe there was a reason it wasn't already in).
To maintain the 960.gs dimensions and classes (as an intermediary step), I made a transitional-960gs.less stylesheet with these rules: <span class="co1">@columns: 16; @column-width: 40; @gutter-width: 20;</span>
. Then I made equivalents of the .grid_X classes (as Clean's implementation had them) with an s_
prefix:
.s_container, .s_container_16 {
margin-left: auto;
margin-right: auto;
width: @total-width;
.clearfix();
}
.s_grid_1 {
.column(1);
}
.s_grid_2 {
.column(2);
}
...
.s_grid_16 {
.column(16);
}
The s_grid_X
classes were purely transitional: they allowed me to do a search-and-replace from grid_ to s_grid_ and remove the 960.gs stylesheet, before migrating all the styles into semantic equivalents. Once that was done, the s_grid_ classes could be removed.
960.gs and semantic.gs also implement their columns a little differently, one with padding and the other with margins, so what was actually a 1000px-wide layout with 960.gs became a 960px layout with semantic.gs. To compensate for this, I made a wrapper mixin applied to all the top-level wrappers:
.wide-wrapper {
.s_container;
padding-right: 20px;
padding-left: 20px;
.clearfix();
}
With the groundwork laid, I went through all the grid_/s_grid_ classes in use and replaced them with purely in-CSS semantic mixins. So if a block had a grid class before, now it only had a semantic ID or class, with the grid mixins applied to that selector.
Once the primary layout was replicated, I could make it "respond" to @media queries, using a responsive.less sheet. For example:
/* iPad in portrait, or any screen below 1000px */
@media only screen and (max-device-width: 1024px) and (orientation: portrait), screen and (max-width: 999px) {
...
}
/* very narrow browser, or iPhone -- note that <1000px styles above will apply here too!
note: iPhone in portrait is 320px wide, in landscape is 480px wide */
@media only screen and (max-device-width: 480px), only screen and (-webkit-min-device-pixel-ratio: 2), screen and (max-width: 499px) {
...
}
/* iPhone - portrait */
@media only screen and (max-device-width: 480px) and (max-width: 320px) {
...
}
Some vitals tools for the process:
- Less.app (for Mac), or even better, the new CodeKit by the same author compiles and minifies the Less files instantly, so the HTML can refer to normal CSS files.
- The iOS Simulator (part of XCode) and Android Emulator (with the Android SDK), to simulate how your responsive styles work on different devices. (Getting these set up is a project in itself).
- To understand what various screen dimensions looked like, I added a simple viewport debugger to show the screen size in the corner of the page (written as a Drupal6/jQuery document-ready "behavior"; fills a #viewport-size element put separately in the template):
Drupal.behaviors.viewportSize = function() {
if (!$('#viewport-size').size()) return;
Drupal.fillViewportSize = function() {
$('#viewport-size').text( $(window).width() + 'x' + $(window).height() )
.css('top', $('#admin-menu').height());
};
Drupal.fillViewportSize();
$(window).bind('resize', function(event){
Drupal.fillViewportSize();
});
};
After three days of work, the layout is now entirely semantic, and the 960.gs stylesheet is gone. On a wide-screen monitor it looks exactly the same as before, but it now adapts to narrower screen sizes (you can see this by shrinking the window's width), and has special styles for iPad and iPhone (portrait and landscape), and was confirmed to work on a popular Android tablet. It'll be a continuing work in progress, but the experience is now much better on small devices, and the groundwork is laid for future tweaks or redesigns.
There are some downsides to this approach worth considering:
- Mobile devices still load the full CSS and HTML needed for the "desktop" layout, even if not all the elements are shown. This is a problem for performance.
- The stylesheets are enormous with all the mixins, compounding the previous issue. I haven't examined in depth how much of a problem this actually is, but I'll need to at some point.
- The contents of the page can only change as much as the stylesheets allow. The order of elements can't change (unless their visible order can be manipulated with CSS floats).
To mitigate these and modify the actual content on mobile devices - to reduce the performance overhead, load smaller images, or put less HTML on the page - would probably require backend modifications that detect the user agent (perhaps using Browscap). I've been avoiding that approach until now, but with most of the work done on the CSS side, a hybrid backend solution is probably the next logical step. (For the images, Responsive Images could also help on the client side.)
See the new layout at work, and my links on responsive design. I'm curious to hear what other people do to solve these issues.
Added: It appears the javascript analog to media queries is media query lists, which are event-able. And here's an approach with media queries and CSS transition events.