詹姆斯 James

Advanced Columned Layouts with CSS

February 26th, 2013

Objective journalism and an opinion column are about as similar as the Bible and Playboy magazine. Walter Cronkite, 1916–2009

Until fairly recently, I’d been working for the Kaldor Group on a product called Pugpig, a publishing platform for mobile devices. Pugpig is a hybrid system, using HTML for the content itself, but native code for the UI and content delivery.

While working with clients in the magazine and news publishing industries, I’d been tasked with producing some fairly complex columned layouts in HTML, which weren’t easily accomplished with CSS1 In this post, I’ll explain a few of the tricks I used to meet those needs.

I’m going to take you step by step through the process of creating a simple three-column layout, with a single image spanning two of the columns, the image being aligned to the top right of the page.

This markup is intended for use within the Pugpig framework, but you can still view the results in most modern browsers and get a feel for how it works.

The markup

For a start, we’ll need some boilerplate markup in the head of our document.

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0,
    user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
  <link rel="stylesheet" type="text/css" href="example.css" />
</head>

The charset meta tag ensures that the native web view knows which character set we’re using, since that can’t be inferred from HTTP headers once the content is packaged up and downloaded to the device.

The viewport settings disable the pinch-to-zoom functionality (that kind of scaling doesn’t make much sense for a columned layout), as well as preventing the web view from messing with our viewport size. 2

In the body, we’ll begin with a simple article element with an h1 title and several paragraphs of text.

<body>
  <article>
    <h1>Lorem ipsum dolor sit</h1>
    <p>Lorem ipsum dolor sit amet...</p>
    <p>Curabitur sodales ligula...</p>
    <p>Nulla metus metus, ullamcorper...</p>
    <p>Lorem ipsum dolor sit amet...</p>
    <p>Curabitur sodales ligula...</p>
    <p>Nulla metus metus, ullamcorper...</p>
    <p>Lorem ipsum dolor sit amet...</p>
    <p>Curabitur sodales ligula...</p>
    <p>Nulla metus metus, ullamcorper...</p>
  </article>
</body>

The basics

Our first step will be to get some fairly standard, fixed-height, CSS columns. We want three columns per page, with a 50px gap between columns, and a 25px margin around the edges. For now we’re going to hardcode the height at 500px.

* {
  margin:0;
}
body {
  margin:25px;
}
article {
  height:500px;
  width:100%;
  column-count:3;
  column-gap:50px;
  column-fill:auto;
}

A key point worth mentioning is that the column-gap must be exactly twice the width of the horizontal margins. The reason for this becomes clear when the article text is too long to fit in the available width.

When viewed in a browser, the content will simply scroll horizontally, but in Pugpig, the article is automatically split into panes, allowing the user to swipe from one pane to the next. When that happens, the gap between columns three and four becomes split across the pane boundary.

The right hand margin of the first pane, and the left margin of the second pane are really just two halves of a column-gap. You can see what I mean in the image below.

A columned article spread across two panes, showing how the column gap is split across the pane boundary.

As for the column-fill property: by default the fill strategy is “balance”, but for this kind of layout we need it to be “auto”. While it doesn’t actually make any difference in current versions of WebKit, it assumedly will in the future (not to mention other browser engines), so it’s important we get it right.

« View Step One »

The line-height

When working with older versions of WebKit (a sad necessity in the mobile industry), if the height of the contents of a column doesn’t exactly match the height of the multi-column container, you’ll often end up with a broken layout in subsequent columns.

To avoid this problem, we need to make sure that everything inside the multi-column container shares the same line-height (or possibly a multiple thereof), and the height of the container itself is also a multiple of that line-height.

In this example, we’re going set our line-height to 25px which is a nice easy number to work with. The height of the container (our article element) has already been set to a multiple of 25.

I’m also going to set the font-size to something a little larger, and set a text-indent to mark paragraph breaks. If you prefer to have a margin break between paragraphs, that’s also OK, as long as it’s exactly one line in height.

article p {
  line-height:25px;
  font-size:1.125em;
  text-indent:1.5em;
}

For the title, we’re going to want a much bigger font-size, so we’re also going to need to double the line-height.

article h1 {
  line-height:50px;
  font-size:2.25em;
}

Note that there are limitations on when you can use a double line-height like this though. The key is to make sure there is no risk of the content appearing on the last line of a column – being double the height, it wouldn’t fit, so you’d potentially be left with a broken layout again.

« View Step Two »

A responsive height

So far we’ve been using a fixed height for the article element, but to be device independent, that height really needs to adjust to fit the available space.

We’ll start by doing this the obvious way – setting the height to 100%. For this to work, we also need to set the height on each containing block, all the way up to the root.

html, body, article {
  height:100%;
}

The next problem, is that the total height actually ends up being bigger than height of the display, because of our margins on the body. But we can fix this by removing the vertical margins, adding them back as padding on the article, and then setting the box-sizing to “border-box” so they’re included as part of the 100% height.

body {
  margin:0 25px;
}
article {
  padding:25px 0;
  box-sizing:border-box;
}

For most browsers, this should be all we need, but as I explained in the previous section, older versions of WebKit can get bent out of shape if the multi-column container height isn’t an exact multiple of the line-height.

Our solution for this is a little tricky. What we do is set the height of the body element to 4%, and the height of the article element to 2500% (that is 25 times the body height).

body {
  height:4%;
}
article {
  height:2500%;
}

When you multiply those two percentages together, you’re still essentially getting 100%, but because older versions of WebKit use integers for their internal calculations, the computed height is guaranteed to be a multiple of 25 (which is to say, our line-height).

This won’t work on newer versions of WebKit and other browser engines that use subpixels for their internal calculations, but they’ll just end up with a height that is exactly 100%. As long as they don’t have the same bugs as the older WebKits, that’s still OK.

« View Step Three »

The bees

Now we get to the fun part. We’re not going to add an image just yet, but we’re going to make space in the column flow where the image will later be inserted. For this, we’re going to need a little more markup: basically a whole lot of b elements.

I like to refer to these as “the bees”.

<article>
  <b></b><b></b><b></b><b></b><b></b><b></b>
  <h1>Lorem ipsum dolor sit</h1>
  ...

Why a b element? One, it’s a single character so it doesn’t take up a lot of space; and two, in HTML5, it’s more-or-less defined as the element of last resort. 3 If you kind of squint your eyes a little, you can almost pretend it’s semantic. Almost.

The important thing, though, is how we’re going to style these bees. We’ll start by floating them to the right, allowing the text in the columns to flow around them.

b {
  display:block;
  float:right;
}

We’ll assign all the odd ones a height of 100% (i.e. the full height of the column) and a width of 1% (just so they’re visible for now). We’ll also make them clear to the right, so that they stack on top of each other – essentially one block in each column.

b:nth-of-type(odd) {
  height:100%;
  width:1%;
  clear:right;
  background-color:red;
}

You’ll notice I’ve also set the background-color, so you can see what is going on.

We’ll leave the even blocks with the clear property unset, so for each odd block, there will be an even block floating to the left of it. Then by giving them a width of 99% (the remaining width of the column), and a height of 250px, we will have created a ten line block of space at the top of every column.

b:nth-of-type(even) {
  height:250px;
  width:99%;
  background-color:silver;
}

Finally, in this particular case, we want our image to span only the last two columns, so we need to hide the first even block (that is the second b element). Using the same markup, we can achieve a number of other layouts, just by hiding different blocks.

b:nth-of-type(2) {
  display:hidden;
}

It’s worth mentioning at this point, that older versions of WebKit won’t get this quite right. You should get the space you need, correctly positioned in the column flow, but you may also see the red and silver blocks overflowing from the bottom of the article, and making the page scroll vertically.

Now the fact that the blocks appear to be in the wrong place isn’t a big deal, since they’ll eventually be invisible, but the vertical scrolling is more of an issue. If you’re using a layout like this in Pugpig, always remember to turn off the scrollEnabled property via the API – this will disable vertical scrolling.

« View Step Four »

The image

Now we get to add the actual image. To keep things simple, we’ll just use a basic img tag. We’re going to be positioning it absolutely, so we can insert it pretty much anywhere in the markup.

...
<h1>Lorem ipsum dolor sit</h1>
<img src="example-image.jpg" alt="Example image" />
<p>Lorem ipsum...</p>
...

The CSS is a little complicated, but I’ll explain everything step by step.

img {
  position:absolute;
  top:25px;
  right:25px;
  height:238px;
  width:66.666%;
  border-left:50px solid transparent;
  box-sizing:border-box;
}

First we’re positioning the image absolutely, relative to the document body4 So to get the image aligned with the top right corner, we need to set the top and right properties to 25px, allowing for the 25px margins.

Second, we’ve set the height to 238px, about half a line shorter than the 250px space we made for the image in the previous step. This is to allow for a small margin between the bottom of the image and the surrounding text.

Finally, we’ve set the width to 66.666% – essentially two thirds of the page. If you think of that in terms of column text and white space (the margins and column gaps), that gives us two columns and two thirds of the white space (100 pixels).

What we really want, though, is two columns plus 50 pixels (for the gap between those columns), so we need to knock off 50 pixels somehow. We do that by adding a 50px border on the left, and setting the box-sizing property to “border-box” (which forces the border to be part of the 66.666% width).

« View Step Five »

The final result

A three-columned article with an image spanning two columns in the top right corner.

All that remains now, is to tidy up the loose ends. We can start by removing the background colours on the bees, since they were just added so we could see how everything worked. Additionally, we can set the 1% width to zero – again that was just to make it visible – and the corresponding 99% width to 100%.

It’s also worth pointing out that my use of unprefixed properties in the CSS was only for the sake of readability. In production code you’ll need to include prefixes for the various column properties as well as the box-sizing property.

If everything has gone as planned, though, the final result should look something like the image above right.

Now of course this isn’t a perfect layout. Ideally we’d want the image to adjust its height to maintain a constant aspect ratio. And it’d be nice if we could support elements that aren’t necessarily an exact multiple of the line-height. But hopefully this is enough to whet your appetite.

If there is sufficient interest, I might cover some of these shortcomings in a future post.

« View Final Result »

1 There are a couple of CSS modules in the works that should make these sorts of layouts much easier (CSS Regions and CSS Template Layout being two examples). However, they’re still very much in the exploratory stage at this point.
2 For more details on the workings of the viewport meta tag, you can read the Safari HTML Reference and/or the W3C Device Adaption spec. Be aware, though, that neither of them is a completely accurate reflection of reality.
3 The exact wording is: The b element should be used as a last resort when no other element is more appropriate.
4 While it would be neater to position the image relative to the article element, that currently triggers a bug in Firefox, and I wanted these examples to work on as many browsers as possible.