Revisiting CSS Vertical Alignment

Sun, Mar 20, 2011

For the most part, the CSS vs Tables battle has been played out and a practical kind of stale-mate has arisen where tables are used for what they were designed for (tabular data), while CSS is used for everything else…

… except for when it simply doesn’t work! Browser bugs, spec ambiguities and developer misunderstandings lead to all sorts of problems with form layouts, complex page layouts and practically any kind of vertical alignment issue.

Which is when those of us with a practical bent simply give up and use tables!

I haven’t visited the CSS vs TABLES debate in this blog yet, and I really don’t intend to discuss it in any serious depth. I will simply say that while I highly value separation of content from presentation, and like using CSS for styling (although I wish it had variables), I am not a fan of using CSS for layout. Neither am I a fan of using TABLES for layout, both approaches have serious drawbacks, but unfortunately they are all we have until the CSS3 layout related modules are more widely available (or until we all go back to native app development!)

However, as the browsers continue to improve, and IE6 finally shuffles off its mortal coil, some of those instances where we might, in the past, resort to tables can be revisited with fresh eyes and see if we can finally let go of some of our legacy ways.

One of those areas where we struggle in CSS is with vertical alignment…

The CSS vertical-align attribute

Example 1: A TABLE

In pre ‘semantic markup’ days, if you had a number of inline elements that needed vertical alignment, you used a table, with the vertical-align: middle style for its cells:

  <style>
    *        { border: 1px solid #1080F0; }
    .small   { font-size: 0.5em; }
    .large   { font-size: 2.0em; }
    #ex1 td  { vertical-align: middle; }
  </style>

  <table id='ex1'> 
    <tr>
      <td><input type='checkbox'></td>
      <td><input type='text'></td>
      <td><span class='small'>small</span></td>
      <td><img src='avatar.gif'></td>
      <td>normal</td>
      <td><span class='large'>large</span></td>
    </tr>
  </table>

Example 2: vertical-align on a DIV

To do this in pure CSS without any tables, a first instinct might be to use vertical-align:middle on a <div>, but as you can see, applying this attribute to a block element has no effect on its children:

  <style>
    #ex2 { vertical-align: middle; }
  </style>

  <div id='ex2'>
    <input type='checkbox'>
    <input type='text'>
    <span class='small'>small</span>
    <img src='avatar.gif'>
    normal
    <span class='large'>large</span>
  </div>

Why? If you read the spec you will see that it only applies to “inline-level and table-cell elements” - This means that the only block level element that it can affect is a table-cell, which we saw in example1.

Example 3: vertical-align on descendants of a DIV

Instead you need to find a way to apply the attribute to the inline children. You can apply vertical-align to each child individually, but its easier with a descendants (*) or child (>) selector.

  <style>
    #ex3 *   { vertical-align: middle; }
  </style>

  <div id='ex3'>
    <input type='checkbox'>
    <input type='text'>
    <span class='small'>small</span>
    <img src='avatar.gif'>
    normal
    <span class='large'>large</span>
  </div>

Again, reading the spec you will see that:

Errr, ok. Thats a bit tricky to understand. The ’line box’ generated by the inline elements is simply a box big enough to contain all of the inline elements. In this case, since our <div> naturally wraps all of the inline elements, that line box is exactly the same as our containing <div> and we get the vertical alignment behaviour that we want.

Example 4: a DIV with height

So far so good, this will work in all modern browsers (Chrome, Safari, FF2+, IE8+), assuming that the browser is in standards mode and not quirks mode (e.g. using a DTD specifying either HTML4.01 or HTML5)

However, it assumes that the <div> itself is flowing naturally around its contained inline elements and is therefore the same size as the generated line box around the inline elements.

But what happens if the <div> itself has a height:

  <style>
    #ex4     { height: 5em; }
    #ex4 *   { vertical-align: middle; }
  </style>

  <div id='ex4'>
    <input type='checkbox'>
    <input type='text'>
    <span class='small'>small</span>
    <img src='avatar.gif'>
    normal
    <span class='large'>large</span>
  </div>

Here we can see that the vertical-align attribute no longer does what we expect.

The reason ? Well, the ’line box’ we have been referring to did not change simply because we gave the containing <div> a height. In the image below we show the outline of this imaginary line box. As you can see the elements are vertically aligned within their line box, but this doesn’t really help us.

Before we examine how to fix this, lets look at another example where vertical-align on child elements does not work as expected:

Example 5: a DIV with child floats

If we go back to example3 where the containing block naturally wraps its child elements, but we introduce some additional floated children, we can see that the floated children do not respect the vertical-align attribute:

  <style>
    .lfloat  { float: left;  border-color: red; }
    .rfloat  { float: right; border-color: red; }
    #ex5 *   { vertical-align: middle; }
  </style>

  <div id='ex5'>
    <div class='lfloat'>lfloat</div>
    <div class='rfloat'>rfloat</div>
    <input type='checkbox'>
    <input type='text'>
    <span class='small'>small</span>
    <img src='avatar.gif'>
    normal
    <span class='large'>large</span>
  </div>

There are actually 2 reasons this doesn’t work. Firstly, the floated elements are block elements and we have already established that vertical-align has no affect on block elements, but even if we switched them to inline elements this example would still break because floated elements do not respect vertical-align either.

NOTE: I have not been able to track down which part of the CSS spec covers vertical-align on floats so if anyone knows exactly why floats don’t respect vertical-align please let me know!

THE SOLUTION: a DIV with line-height

These 2 problems can be solved with the same CSS answer, and that answer is to apply a line-height to the containing block element:

  <style>
    #ex6     { height: 5em; line-height: 5em; }
    #ex6 *   { vertical-align: middle; }
  </style>

  <div id='ex6'>
    <div class='lfloat'>lfloat</div>
    <div class='rfloat'>rfloat</div>
    <input type='checkbox'>
    <input type='text'>
    <span class='small'>small</span>
    <img src='avatar.gif'>
    normal
    <span class='large'>large</span>
  </div>

The explanation for this solution can again be found in the spec:

What this means, in English, is that the line-height on the <div> sets a minimal height on the elements within it, thus forcing the invisible generated ’line box’ to be the same as the height of the <div> and that gives us the vertical alignment behavior needed.

To Summarize…

… don’t use vertical-align on block elements (except table cells). Instead, use it on the child inline elements and they will become aligned within their generated line box.

Additionally, you might need a line-height on the container in order to get the generated line box to match the container’s height.

Helpful Articles

Yup. Sometimes you gotta just read the spec:

One day, one happy day, we will finally be able to use some of these:

Where my tendencies lie in the css vs tables debates: