What it is

A css file and markup technique for putting form fields into a regular, columnal format that degrades gracefully without css by the use of css 2.1 table display property values.

How to get it

See this gist. You can see it in action at this jsFiddle, but the gist is better for downloading since it always gives you the latest version while the same is not true of the jsFiddle.

How to use it

Add the css to the rest of the css for your site. When creating a form, decide whether you’ll be using lists or divs. The basic list version looks like:

<form>
  <ul class="tel-tabular">
    <li> <!-- each li is a row -->
      <label>Field 1:</label><input>
      <label>Field 2:</label><input>
    </li>
    <li>
      <label>Field 3:</label><input>
      <label>Field 4:</label><input>
    </li>
  </ul>
</form>

For more details, read the rest of the article.

Introduction

In this article I’ll show you how to use css to turn form markup into a nice table layout. The great thing about tables is that all of the cells in a row get sized properly to align with everything else in their column. Forms look particularly nice this way they are usually tabular, with labels in the first column and inputs in the second. You may even want to have more than one field in a row, but you still want the columns to size to the width of their widest content.

There are a number of other techniques to layout forms, and they keep evolving. This technique has the advantages that it’s truly table-based without the table markup. You get automatic column sizing, so you don’t need to know the width of your content a priori like you do with other techniques. This is especially handy if you simply don’t know the width of your content. For example, you may be pulling field names from a database or you may be pulling translations for field names for internationalization purposes. The table approach works best for these situations.

I’ll also show you how to handle some of the common exeptions to the layout without blowing the layout as a whole. For example, you may need to have one row in the midst of others whose cell size would be much shorter or longer than you’d want for the others. I’ll show you how to handle those rows while still keeping the preceding and following rows connected in the same table, so the column widths apply throughout. I’ll also show you how to put fieldsets in the flow of the form without breaking things into separate tables.

This approach is fairly flexible in the markup it accepts. You can do div-based forms or list-based forms, although you may have to adapt your normal method to the structure required here. In any case, this flexibility is designed so you can write markup which degrades gracefully without css, so you get something that is perhaps not as pretty, but still true to the spirit of the layout.

This technique works for current versions of Chrome, IE and Firefox, with minor tweaking of some widths for the advanced features. I don’t know how far back you need to go before you start to lose support. Anything that supports css 2.1 should work.

I haven’t gone into any of the explanations behind the css because while I meant to, the time consumed in developing it proved to be too much so I’m posting what I can. I hope to come back to it and do a more detailed explanation.

Why not html tables, floats or inline-blocks?

The historical ways that you can create a multi-column table-like layout is to use one of these three methods. However:

  • Html tables are frowned upon for anything other than tabular data by just about everyone nowadays.
  • With floats, it’s difficult to get columns to match in height like table columns do automatically. Floats end up being column-oriented and I want row-oriented markup which is degrades gracefully to non-styled layout.
  • Inline-blocks are pretty good, but require you to know the widest width of a given column and set a minimum-width on all of the elements. If you are creating content on-the-fly, you can’t know its width a priori. That can and will blow the layout. Where it really falls down are internationalized apps where you may have no idea what your widths are going to be.

Basic form

I designed the css for either list-based or div-based forms. I’ll show list markup then explain the div version.

This example supposes that you want a four column form, one column each for a couple label/input pairs.

The markup:

<form>
  <ul class="tel-tabular">
    <li> <!-- each li is a table row -->
      <label>Field 1:</label>
      <input>
      <label>Field 2:</label>
      <input>
    </li>
    <li>
      <label>Field 3:</label>
      <input>
      <label>Field 4:</label>
      <input>
    </li>
  </ul>
</form>

What you get here is a two-row table with two label/input pairs per row. If you change the content of any element, the layout will adjust and keep everything in columns as you would expect of a table.

The css:

.tel-tabular > * {
  display: table-row;
}

.tel-tabular > * > * {
  display: table-cell;
  padding-right: 0.3em;
}

.tel-tabular input {
  margin-right: 1em;
}

label {
  text-align: right;
}

I’ve put in some of my own preferences for display such as right-alignment of labels, etc.

Note that you can just as easily make a 6-column layout (or any you want) just by adding elements to each li.

Using divs instead of ul

If you prefer divs, just substitute them for the ul and li elements like so:

<form>
  <div class="tel-tabular">
    <div> <!-- each div under .tel-tabular is a table row -->
      <label>Field 1:</label>
      <input>
      <label>Field 2:</label>
      <input>
    </div>
    <div>
      <label>Field 3:</label>
      <input>
      <label>Field 4:</label>
      <input>
    </div>
  </div>
</form>

In fact, you don’t even need the enclosing div. You can eliminate it and apply the tel-tabular class to the form itself.

You can use divs instead of uls for the rest of the examples in the same way, so I won’t bother showing both methods.

Overflowed cells

You can make an element that streches outside of its cell so that you can handle unusual situations that might normally break the flow of the table as a whole.

The markup:

<form>
  <ul class="tel-tabular">
    <li>
      <label>Field 1:</label>
      <input>
      <label>Field 2:</label>
      <input>
    </li>
    <li>
      <span>
        <span>
          <input type="checkbox">
          <label>A checkbox that has a long label</label>
        </span>
      </span>
    </li>
  </ul>
</form>

The structure is the same as you would expect for the first row from the first example. The second row, however, holds a checkbox and label that will look too far spaced apart if they are put in separate cells, but won’t fit inside a single cell either.

This markup will put them in a single cell but allow them to overflow outside the cell without affecting the rest of the table. Notice the two spans around the items that will extend outside of their cell. They will allow the css to do the proper formatting.

The css for it is in addition to the earlier css:

.tel-tabular > * > * > * {
  display: inline-block;
  width: 0;
  white-space: nowrap;
}

Note that if you want to add further content to the same row, you will have to pad the row with blank cells until you reach one that doesn’t overlap with the overflow.

You can do this with empty cells:

<li>
  <span>
    <span>
      <input type="checkbox">
      <label>A checkbox that has a long label</label>
    </span>
  </span>
  <span>&nbsp;</span>
  <span>&nbsp;</span>
  <label>Some more content</label>
</li>

Short fields

You might also want to put multiple fields in a cell where space permits. The classic example of this is the state and zip fields in an address.

We’ll make the label for the state field be the first cell as usual. We’ll then use the overflowed cell technique to place the state input as well as the zip label and input into the following cell. In this case, we’re not actually overflowing the cell but the technique works to prevent each individual content item from being assigned to its own cell.

This time we’ll only need css for defining our input widths, but we’ll need new classes for that purpose.

The additional css:

.tel-tabular .tel-state {
  width: 2em;
  margin-left: 0.2em;
  margin-right: 0.3em;
}

.tel-tabular .tel-zip {
  width: 5em;
}

Note that we’ll also need some margins on the first input to get the second one to line up on the right edge of the column. The rendering tends to be browser-specific, so choose which browser you want to optimize for and accept that it isn’t going to be perfect on the others. Or figure out a better method than this one. :)

Also note that I’ve included the .tel-tabular qualifier on both selectors. You wouldn’t have to do this normally, but in this case I’m planning ahead to avoid conflicts with the css I’ll be introducing in the next section.

The markup:

<form>
  <ul class="tel-tabular">
    <li>
      <label>Field 1:</label>
      <input>
      <label>Field 2:</label>
      <input>
    </li>
    <li>
      <label>State:</label>
      <span>
        <span>
          <input class="tel-state">
          <label>Zip:</label>
          <input class="tel-zip">
        </span>
      </span>
    </li>
  </ul>
</form>

You can still put further fields on the same line by adding them to the same li. You can pad out blank cells with the   method as well.

Fieldsets

Unfortunately, if you put a real fieldset in the middle of the form, it divides the form into separate tables before and after the fieldset. I couldn’t find a way to keep this from happening, and it ruins the gestalt of the form.

You can make separate tables look the same by assigning minimum widths, but then we’re back to the inline-block method anyway, which I didn’t want. Instead, I’ll fake a fieldset by mocking it up with a div.

The markup:

<form>
  <ul class="tel-tabular">
    <li>
      <label>Field 1:</label>
      <input>
      <label>Field 2:</label>
      <input>
    </li>
    <li class="tel-fieldset">
      <div class="tel-legend"><div>Legend text</div></div>
      <div>
        <label>Field 3 (with filler):</label>
        <input>
        <label>Field 4:</label>
        <input>
      </div>
    </li>
    <li>
      <label>Field 5:</label>
      <input>
      <label>Field 6:</label>
      <input>
    </li>
  </ul>
</form>

Here the li gets the fieldset class. It includes a legend div (with an embedded div to give the css what it needs). The other div contains the rows for the fields. We have to use divs for rows instead of lis at this point, but it still degrades nicely without css. Without css, the legend visibly marks off the fields as being separate, and they are gathered under a single bullet.

The additional css is a bit more complicated, partly because it has to support the capabilities of overflowed and short cells. It also has to neutralize some properties that bleed onto it due to the use of * selectors in the earlier css, but it’s pretty harmless and I wanted to allow you to choose between uls and divs as you see fit, so it’s worth the bit of additional complexity.

Here it is:

.tel-tabular > * {
  display: table-row-group;
}

.tel-tabular {
  border-collapse: collapse;
}

.tel-fieldset {
  border: 1px solid;
}

.tel-fieldset > * {
  display: table-row;
  padding-right: 0;
}

.tel-fieldset > * > * {
  display: table-cell;
  white-space: inherit;
  padding-right: 0.3em;
  width: auto;
}

.tel-fieldset > * > * > * {
  display: inline-block;
  width: 0;
  white-space: nowrap;
}

.tel-legend {
  position: relative;
  display: block;
}

.tel-legend > * {
  position: absolute;
  top: -0.74em;
  left: 0.6em;
  display: block;
  background: white;
  padding: 0 0.3em;
  white-space: nowrap;
}

label {
  padding-bottom: 0.7em;
  padding-top: 0.6em;
}

Notice that we’ve changed the display of .tel-tabular > * from table-row to table-row-group. This allows us to add a level of rows under the .tel-fieldset element that are still rows and still align with the columns. In the css in this gist I just use the table-row-group value in the first place. For this article I just wanted to show the progression of the css as we added features.

Also notice the padding added to the label element. This is so that the legend text doesn’t cramp the lines above and below it. This is the easy way out since I’m adding that padding to every line, not just the ones before and after the legend. If someone can figure out an elegant way to just pad the legend, I think that would be better. Another (ugly) method is to add a row of empty cells before and after the fieldset with a div full of of nbsp spans. You’ll probably want to add some around the ending div tag of the fieldset as well. It looks nice when rendered, but not very good practice for html.

The legend has a background which has to be set in order to a color to mask the fieldset border, so choose one that matches your page background. It can’t be transparent because the border will show.

There are some gotchas with these mock fieldsets which I’ve encountered in practice.

The fieldset border will have a gap if there is no cell in the final column of your row. Stub out all fieldset rows with blank   cells to fix this.

The top of the fieldset border also requires that the table-row-group before it has all of the cells in a row. That means that if it’s the first thing in the form, you have to put in a dummy div before it which contains the right number of nbsp spans.

Other Caveats

While automatic table layout is great, it does come with it’s own set of rules. The biggest issue I’ve run into is that you can’t set margin on most table elements, including rows and the table itself. Some items with display: table-cell will let you set margin though, such as the labels. This can be a big drawback of this method.

The other table-related caveat is that setting height of 100% doesn’t work on table cells. Explicit heights in px or ems seems to work fine, but I’m not sure if it’s just percentages or 100% in particular.

If you are used to using nested lists to define fieldset-like areas in your forms, you will have to use divs nested inside the lists instead. The structure of the css requires it, and I can’t see a way to allow nested lists without breaking the table flow.

Because overflowed cells are width 0, trying to use one on the right side of a fieldset will cause it to overflow the border, so you don’t want to do this. Keep overflowed cells on the left side of your table where they will have space from following columns to rely on. Short fields which are attached to the widest field in the rightmost column will similarly overflow the fieldset.

The last caveat is that you’ll need to tweak the css to match the feel of your design through the use of color, fonts, padding, etc. You may also have to tweak the padding on labels to ensure that the legend on fieldsets isn’t overlapping the rows before and after. Since this is likely to result in slight variations between browsers, choose which browser you want to target and develop for that one first.

Don’t forget to test your results on the browsers you care about.

Bonuses

Because these are real tables, you get vertical alignment for free, which is not true of other layout methods. I don’t use it in forms but I do use it when I do page layouts, for example.

Conclusion

That’s how to achieve table-based layouts with forms. Tables have the advantage of automatically accomodating the width of their content, which makes them ideal for laying out forms where the content width is not necessarily known beforehand.

You can choose between div-based layout and list-based layout. The layout degrades gracefully without css, so the form is still usable in that case and friendly to assistive technologies.

You can designate some cells to be short or long without affecting the width of their column through a combination of css and markup. The rest of the table before and after keep their flow and still look like a unified table.

The same is true of fieldsets through the use of our mock fieldsets done in css. While you can’t use real fieldsets without breaking the flow of the table before and after, you can use the mock fieldset and it looks close to the same.

That’s it. Please drop me a comment if you find this useful!