CSS Framebox Tables
By: Larry Klug
--- 04/24/2010

Often, we need our web applications to display complex visual presentation effects such as drop shadows and rounded corners in a way that is cross-browser safe and has predictable behavior. The old-school approach is to use a 9 cell (3x3) HTML table structure as a container that can be decorated using a series of background images that represent each segment of the 3x3 matrix, top-left, top-middle, top-right, etc. Although semantic web purists profess that this approach is incorrect and favor styled block level elements such as div and span, it is largely due to the predictable page crush behavior and simplicity of debugging that has driven to our decision to continue with styled tables as our complex box formatting presentation pattern.

A framebox table pattern that we use today looks something like this:

The table is made up of a 3x3 matrix that when divided looks something like this:

Each of the sections is decorated using an individual image, named with the relative grid position and color.

The code used to render this table is as follows:


   1: <table cellpadding="0" cellspacing="0">
   2: <tr>
   3: <td><img src="images/TitleFrameOrange_UL.gif" /></td>
   4: <td align="center" class="FrameTitle" background="images/TitleFrameOrange_Top.gif">
   5: <table width="100%">
   6: <tr>
   7: <td class="FrameTitle" align="center">Emergency Broadcast System</td>
   8: </tr>
   9: </table>
  10: </td>
  11: <td><img src="images/TitleFrameOrange_UR.gif" /></td>
  12: </tr>
  13: <tr>
  14: <td height="100%" background="images/TitleFrameOrange_Left.gif"></td>
  15: <td valign="top" height="100%" class="FrameContents" background="images/TitleFrameOrange_Center.gif">
  16: <center>
  17: <br />
  18:                  This is a test of the emergency broadcast system. The broadcasters in your area, in voluntary cooperation with<br />
  19:                  the FCC and other local authorities have developed this test to keep you informed in the event of an emergency.
  20: <br />
  21: </center>
  22: </td>
  23: <td height="100%" background="images/TitleFrameOrange_Right.gif"></td>
  24: </tr>
  25: <tr>
  26: <td><img src="images/TitleFrameOrange_LL.gif" /></td>
  27: <td background="images/TitleFrameOrange_Bottom.gif"></td>
  28: <td><img src="images/TitleFrameOrange_LR.gif" />td>
  29: </tr>
  30: </table>


As you can see, this table manages nearly all of the visual presentation instructions directly in the markup. There are only two CSS selectors named, and the images are presented in the markup and therefore must be individually retrieved on every page request. In order to change the color of the framebox, the filename of each of the 9 segments must be changed.

In order to reduce the effort needed to change the frame color, reduce the overall markup delivery and allow for all images to be delivered only once and cached by the browser, I began refactoring by stacking two framebox tables on top of each other, and adjusting the markup of only the first table, comparing after each modification.

I stripped all of the table markup with the exception of the basic table primitives and gave every cell a distinct class representing the position, "ul" for "upper-left", "um" for "upper-middle" and so forth. I then added a primary class selector for the table itself. I then created a series of CSS selectors that correspond to each of the cells in the matrix. By replicating each of the visual instructions from the original table markup with their CSS counterparts, I was able to visually match the rendering of both tables.

By using multiple class selectors on the table class element, I was able to specify the overall table presentation elements (heights, widths, padding, etc.) with the first selector and the color in the second selector. In this way, I could change the color of the entire frame by changing only a single color selector.

The resulting CSS is as follows:


   1:  .FrameBox
   2:  {
   3:      border-spacing: 0px;
   4:      border-collapse: collapse;
   5:      padding: 0px;
   6:      margin: 0px;
   7:  }
   8:  .FrameBox td
   9:  {
  10:      padding: 0px;
  11:  }
  12:      .FrameBox .ul
  13:      {
  14:          width: 10px;
  15:          height: 28px;
  16:      }
  17:      .FrameBox .um
  18:      {
  19:          text-align: center;
  20:          height: 28px;
  21:          background-repeat: repeat-x;
  22:          color: #f8f8f8;
  23:          font-weight: bold;
  24:      }
  25:      .FrameBox .ur
  26:      {
  27:          width: 10px;
  28:          height: 28px;
  29:      }
  30:      .FrameBox .ml
  31:      {
  32:          width: 10px;
  33:          background-repeat: repeat-y;
  34:      }
  35:      .FrameBox .mm
  36:      {
  37:          text-align: center;
  38:          background-repeat: repeat-x;
  39:          vertical-align: top;
  40:          padding-top: 1.7em;
  41:          padding-left: 2px;
  42:          padding-right: 2px;
  43:          line-height: 160%;
  44:      }
  45:      .FrameBox .mr
  46:      {
  47:          width: 10px;
  48:          background-repeat: repeat-y;
  49:      }
  50:      .FrameBox .ll
  51:      {
  52:          width: 10px;
  53:          height: 12px;
  54:      }
  55:      .FrameBox .lm
  56:      {
  57:          text-align: center;
  58:          background-repeat: repeat-x;
  59:      }
  60:      .FrameBox .lr
  61:      {
  62:          width: 10px;
  63:          height: 12px;
  64:      }
  66:      /* colors */
  68:      .blue .ul {background: url("../images/TitleFrameBlue_UL.gif");}
  69:      .blue .um {background: url("../images/TitleFrameBlue_Top.gif");}
  70:      .blue .ur {background: url("../images/TitleFrameBlue_UR.gif");}
  71:      .blue .ml {background: url("../images/TitleFrameBlue_Left.gif");}
  72:      .blue .mm {background: url("../images/TitleFrameBlue_Center.gif");}
  73:      .blue .mr {background: url("../images/TitleFrameBlue_Right.gif");}
  74:      .blue .ll {background: url("../images/TitleFrameBlue_LL.gif");}
  75:      .blue .lm {background: url("../images/TitleFrameBlue_Bottom.gif");}
  76:      .blue .lr {background: url("../images/TitleFrameBlue_LR.gif");}
  78:      .green .ul {background: url("../images/TitleFrameGreen_UL.gif");}
  79:      .green .um {background: url("../images/TitleFrameGreen_Top.gif");}
  80:      .green .ur {background: url("../images/TitleFrameGreen_UR.gif");}
  81:      .green .ml {background: url("../images/TitleFrameGreen_Left.gif");}
  82:      .green .mm {background: url("../images/TitleFrameGreen_Center.gif");}
  83:      .green .mr {background: url("../images/TitleFrameGreen_Right.gif");}
  84:      .green .ll {background: url("../images/TitleFrameGreen_LL.gif");}
  85:      .green .lm {background: url("../images/TitleFrameGreen_Bottom.gif");}
  86:      .green .lr {background: url("../images/TitleFrameGreen_LR.gif");}
  88:      .grey .ul {background: url("../images/TitleFrameGrey_UL.gif");}
  89:      .grey .um {background: url("../images/TitleFrameGrey_Top.gif");}
  90:      .grey .ur {background: url("../images/TitleFrameGrey_UR.gif");}
  91:      .grey .ml {background: url("../images/TitleFrameGrey_Left.gif");}
  92:      .grey .mm {background: url("../images/TitleFrameGrey_Center.gif");}
  93:      .grey .mr {background: url("../images/TitleFrameGrey_Right.gif");}
  94:      .grey .ll {background: url("../images/TitleFrameGrey_LL.gif");}
  95:      .grey .lm {background: url("../images/TitleFrameGrey_Bottom.gif");}
  96:      .grey .lr {background: url("../images/TitleFrameGrey_LR.gif");}
  98:      .red .ul {background: url("../images/TitleFrameRed_UL.gif");}
  99:      .red .um {background: url("../images/TitleFrameRed_Top.gif");}
 100:      .red .ur {background: url("../images/TitleFrameRed_UR.gif");}
 101:      .red .ml {background: url("../images/TitleFrameRed_Left.gif");}
 102:      .red .mm {background: url("../images/TitleFrameRed_Center.gif");}
 103:      .red .mr {background: url("../images/TitleFrameRed_Right.gif");}
 104:      .red .ll {background: url("../images/TitleFrameRed_LL.gif");}
 105:      .red .lm {background: url("../images/TitleFrameRed_Bottom.gif");}
 106:      .red .lr {background: url("../images/TitleFrameRed_LR.gif");}
 108:      .orange .ul {background: url("../images/TitleFrameOrange_UL.gif");}
 109:      .orange .um {background: url("../images/TitleFrameOrange_Top.gif");}
 110:      .orange .ur {background: url("../images/TitleFrameOrange_UR.gif");}
 111:      .orange .ml {background: url("../images/TitleFrameOrange_Left.gif");}
 112:      .orange .mm {background: url("../images/TitleFrameOrange_Center.gif");}
 113:      .orange .mr {background: url("../images/TitleFrameOrange_Right.gif");}
 114:      .orange .ll {background: url("../images/TitleFrameOrange_LL.gif");}
 115:      .orange .lm {background: url("../images/TitleFrameOrange_Bottom.gif");}
 116:      .orange .lr {background: url("../images/TitleFrameOrange_LR.gif");}


The resulting markup is as follows:


   1: <table class="FrameBox orange">
   2: <tr>
   3: <td class="ul"></td>
   4: <td class="um">Emergency Broadcast System</td>
   5: <td class="ur"></td>
   6: </tr>
   7: <tr>
   8: <td class="ml"></td>
   9: <td class="mm">
  10:              This is a test of the emergency broadcast system. The broadcasters in your area, in voluntary cooperation with<br />
  11:              the FCC and other local authorities have developed this test to keep you informed in the event of an emergency.
  12: </td>
  13: <td class="mr"></td>
  14: </tr>
  15: <tr>
  16: <td class="ll"></td>
  17: <td class="lm"></td>
  18: <td class="lr"></td>
  19: </tr>
  20: </table>


As you can see, significantly less markup is delivered, and all images and visual instructions are contained in the style sheet, delivered only once and cached by the browser. By changing the color selector from "orange" to "blue" for example, the entire set of appropriately colored images are used.

Final reference rendering of both the original table and the markup friendly version appears as close to identical as I care to make it, and should behave exactly the same as the original table when subjected to crush behavior or containing other markup in the center cell.

Return to Blog