Displaying a Table With Frozen Headers
  1. Overview
  2. Pseudo Code
    1. The style section
    2. The javascript function
    3. The data
  3. Example

Overview

This note describes a light-weight (only one small javascript function) approach to display large scrolling tables where the column and/or row headers remain visible at all times, much like frozen panes in a spreadsheet program.

This is a 'minimal fuss' implementation that relies on moving and clipping the CSS boxes representing the column and row headers to reflect the position of the scrollbars in the main data table.

The layout looks as follows:

                +---------------------+
                | ColumnHeaders table |
    +-----------+---------------------+
    | RowHeaders|  Data table         |
    | table     |                     |
    +-----------+---------------------+

where all three (HTML) tables are encapsulated in a common container (with CSS attribute position:relative, so that the whole construction can be positioned in the 'flow' of the document).

The Data table has class 'data' which has the CSS position attribute set to 'relative', ensuring scrollbars if the data do not fit in the allocated box.

The RowHeaders (class 'row-headers') and ColumnHeader (class 'column-headers') tables are 'scrolled' programmatically by a javascript 'scroll_headers' function which is described below.

The main idea is that e.g. 'scrolling' the ColumnHeaders table is accomplished by

  1. Moving its box to the left (and widening it so that its right border stays in the same place)
  2. Defining a clipping area on the ColumnHeaders box that is 'constant' (and in the correct position on top of the Data table). However, since the clipping area is defined relative to the upper left corner of the box, it should be redefined on each scroll operation.

Pseudo Code

The following pseudo code contains some more details: it uses the following 'variables', which should be replaced by actual numbers in a concrete example:

ROW_HEADERS.WIDTHThe width of the box displaying the RowHeaders table.
COLUMN_HEADERS.HEIGHT The height of the box displaying the ColumnHeaders table.
DATA.WIDTHThe width of the box displaying the Data table
DATA.HEIGHTThe height of the box displaying the Data table
DATA_TABLE_TOTAL_WIDTHThe 'real' width of the Data table.

The style section

   /* It is important to have 'table-layout' fixed, at least for
    * the Data and ColumnHeaders tables. This way, the browser will
    * compute the size of the columns from the total width of the
    * table and the number of columns. Hence, one should ensure
    * that
    *   Data.width = ColumnHeaders.width
    * and, naturally, that
    *   Data and ColumnHeaders have the same number of columns.
    */
   table { table-layout: fixed; }
   td { color:blue; } 
   th { color:red; }
   /* The whole table sits in a container which has position relative
    * so that coordinates of the table are relative to this
    * container's box.
    */
   div.container { position: relative; }
   /* The 'overflow:hidden' attribute specification ensures that the
    * headers do not extend to the right of their allocated window.
    * The 'left' attribute is such that it leaves room for the
    * row-headers to the left of the column-headers.
    * The left/top/height/width attributes define a 'viewing box'
    * for the ColumnHeader table: in particular, the 'width' attribute
    * limits the number of visible columns. Note that
    *    column-headers.width == data.width
    * and
    *    column-headers.height == row-headers.top == data.top
    * should be true.
    */
   div.column-headers { 
     position:absolute; 
     overflow:hidden;
     left:ROW_HEADERS.WIDTHpx; top:0px; height:COLUMN_HEADERS.HEIGHTpx; width:DATA.WIDTHpx; 
     }
   /* Similar to the row-headers style. Here the 'height' attribute
    * is important: 
    *   row-headers.height == data.height and
    * Also, 
    *   row-headers.width == column-headers.left == data.left == RowHeaders.width  
    * should hold.
    */
   div.row-headers { 
     position:absolute; 
     overflow:hidden;
     left:0px; top:COLUMN_HEADERS.HEIGHTpx; height:DATA.HEIGHT; width:ROW_HEADERS.WIDTHpx; 
     }
   /* Style for the actual data table. The 'overflow:auto' attribute
    * ensures that a scrollbar will appear if necessary.
    * The values for the left/top/height/width attributes
    * follow from the above equalities.
    */ 
   div.data {
     position:absolute; 
     overflow:auto;
     left:ROW_HEADERS.WIDTHpx; top:COLUMN_HEADERS.HEIGHTpx; height:DATA.HEIGHT; width:DATA.WIDTHpx;
     }

The javascript function

   /* This function handles scrolling events by adjusting the display
    * of row-headers or column-headers.
    */
   function scroll_headers() {
     var h_delta = document.getElementById("Data").scrollLeft
     /* The data table was scrolled (horizontally) by h_delta px. 
      * Conceptually, this means that 
      * the box containing the data has moved to the left by h_delta px.
      * We do the same with the ColumnHeaders by
      * - setting the width of its box to data.width + h_delta
      * - moving the left border of its box to data.left - h_delta
      * The clipping window (specified by the 'clip' style attribute) 
      * of the ColumnHeaders should however stay
      * 'in the same place'. But, since the clip area
      *         rect(top, right, bottom, left)
      * is defined relative to the top left corner of the box,
      * it should be redefined to stay in the same area of the
      * screen. Specifically, the 'left' border of the clipping
      * area is now at h_delta px and the 'right' border is
      * at data.width + h_delta . The 'top' and 'bottom'
      * border of the clipping area should be constant
      * where top = 0px and bottom = column-headers.height.
      *
      * Note that firefox (2.0.0.2) does not seem to handle
      * statements like
      *   xxx.style.clip.left =
      * but instead expects a CSS type specification of the
      * form
      *   xxx.style.clip = "rect(top, right, bottom, left)"
      */
     document.getElementById("ColumnHeaders").style.width = (DATA.WIDTH+h_delta) + "px"
     document.getElementById("ColumnHeaders").style.left = (ROW_HEADERS.WIDTH-h_delta) + "px"
     document.getElementById("ColumnHeaders").style.clip = 
       "rect(0px,"+(DATA.WIDTH+h_delta)+"px,"+"COLUMN_HEADERS.HEIGHTpx,"+h_delta+"px"+")"

     /* The data table was scrolled (vertically) by v_delta px. 
      * Conceptually, this means that 
      * the box containing the data has moved up by v_delta px.
      * We do the same with the RowHeaders by
      * - setting the height of its box to data.height + v_delta
      * - moving the top border of its box to data.top - v_delta
      * The clipping window (specified by the 'clip' style attribute) 
      * of the RowHeaders should however stay
      * 'in the same place'. But, since the clip area
      *         rect(top, right, bottom, left)
      * is defined relative to the top left corner of the box,
      * it should be redefined to stay in the same area of the
      * screen. Specifically, the 'top' border of the clipping
      * area is now at v_delta px and the 'bottom' border is
      * at data.height + v_delta . The 'left' and 'right' borders
      * of the clipping area should be constant, i.e.
      * left = 0px, right = row-headers.width.
      */

     var v_delta = document.getElementById("Data").scrollTop
     document.getElementById("RowHeaders").style.height = (DATA.HEIGHT+v_delta) + "px"
     document.getElementById("RowHeaders").style.top = (COLUMN_HEADERS.HEIGHT-v_delta) + "px"
     document.getElementById("RowHeaders").style.clip = 
       "rect("+ v_delta + "px," +"ROW_HEADERS.WIDTHpx,"+ (DATA.HEIGHT+v_delta) + "px,"+"0px"+")"
   }

The data

  <div class="container">
  <div class="data" id="Data" onscroll="scroll_headers()">
   <table border="1px" width="DATA_TABLE_TOTAL_WIDTHpx">
   ..
   </table>
  </div>
  <div class="column-headers" id="ColumnHeaders">
   <table border="1px" width="DATA_TABLE_TOTAL_WIDTHpx">
   ..
   </table>
  </div>
  <div class="row-headers" id="RowHeaders">
   <table border="1px" width="ROW_HEADERS.WIDTHpx">
   ..
   </table>
  </div>
 </div>

Example

A heavily annotated example can be found in example-table.html.

Another, more convenient and probably more powerful, but also much more resource-hungry, complicated and heavy approach to the problem can be found at Cross-Browser.com. In one aspect, the present approach is more general, though: the row headers table can contain more than one column, and similarly for the column headers table.


Dirk Vermeir (dvermeir@vub.ac.be) [Last modified: Wed Mar 14 22:09:01 MET 2007 ]