CSS expressions in IE and Firefox (or, How to make a TBODY scroll without Javascript)
[Update 20060511] I recently moved to my own server (finally!) so I can finally give live samples of the things I'm talking about here. I can't do it inline, as the blog engine will block what's needed - but in each case where I show some sample HTML, there's now a link to a static page that demonstrates what's going on. sorry it's taken so long for this to happen.
[Update 20060510] I've finally updated the inline html samples to have correct closing tags. I'm not sure how they got corrupted in the first place (they worked when I first posted it), but I think I've caught them all. Any more problems, let me know in the comments.
The Why
A couple of months ago, I blogged about a cool way to make a HTML table body scroll in IE using CSS expressions.
That post is by far the most hit page I've written - it seems that many people are trying to figure this out. Since it's been so popular, I wanted a chance to redo what I posted before - I've learned a bit more since then, and I'd also like to address all the comments that have been left on the first post up to now. I've also discovered a new way to do the scrolling that doesn't require the CSS expression at all.
The What
I really want to cover three things in this article.
- The best way I've found to make a HTML TBODY scroll in IE6;
- The best way I've found to do this when you need to support IE5 (and probably 4)
- How I've done it in Firefox.
The How
IE6
To leave out the expressions you have to demand IE6 only, but it's also the simplest and most elegant.
With IE6 came the concept of 'strict standards compliance mode'. Earlier versions of IE had a broken 'box model', in that heights and widths of elements on the page were calculated differently to the CSS specifications. As a result, the 'box model hack' was born, which took advantage of CSS parsing behaviours to get IE to read one style and other browsers a different one, all in the one file.
'Strict standards compliance mode' causes IE to render these things correctly (according to the specification at least - us IE only developers consider all other browsers to still be broken :) Along with the box model, strict compliance mode changes a few other behaviours of IE as well. One of those changes gives up simple TBODY scrolling.
You can read about the changes that strict compliance mode brings into effect on the MSDN website. It also explains how to enable strict compliance mode by authoring your !DOCTYPE correctly.
I chose to use HTML 4.0 Transitional in the following examples, with a reference to the correct DTD. This makes it easy to test between modes - remove the reference to the DTD and you're back into 'quirks mode' and your table won't scroll correctly any more.
So, what does a scrolling TBODY look like in strict compliance mode?
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>scrolling tbody</title>
<style type="text/css">
.container
{
border: solid 1px black;
width: 400px;
height: 150px;
overflow: auto;
}
.noscroll
{
position: relative;
background-color: white;
}
th
{
text-align: left;
}
</style>
</head>
<body>
<div class="container">
<table border="0" cellpadding="0" cellspacing="0" style="width: 384px">
<thead>
<tr class="noscroll">
<th>Col 1</th><th>Col 2</th><th>Col 3</th><th>Col 4</th><th>Col 5</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
</tbody>
</table>
</div>
</body>
</html>
[See this live] - This only works in IE6. I've also tested in IE7B2, and it works fine there as well. In Firefox, it doesn't work at all.
There's no CSS expression! And that's why I like this version.
In effect, what we're rendering is a DIV with a fixed height, deliberately shorter than the height of the TABLE that it contains. The overflow style of the container DIV set is set to auto, which means that the DIV will scroll if necessary - and this example it does. It's also fixed at 400 pixels wide.
Since we've entered strict mode, the box model problems that are inherent in earlier versions of IE (and version 6 when strict mode isn't enabled) aren't with us - so the width of the table is set to 16 pixels less than the container DIV to allow room for the scrollbar. Obviously if you didn't know in advance if your table was actually going to scroll, you would set the overflow style on the DIV to scroll, which will cause the scrollbar to be rendered whether the the table scrolls or not.
The trick to making the header stay still when you scroll is the position: relative; style that is part of the noscroll class. When you explicitly set relative positioning on an element when one of it's parents has an overflow style set, then what you've really done is caused it's position to be fixed.
And that's it! If you can limit your users to only browsing with IE6, and you can stay in strict compliance mode, you're done. Microsoft have very little support for IE5 or less browsers anymore, and IE6 is, afterall, about 4 years old now, so IE 5 is starting to get pretty rare. Staying in strict compliance mode is a little harder, but worth it as far as I'm concerned.
I owe my discovery of this fact to 'boconnor' who left a comment on my first post. He gave a link to this article where the author has some tricks for scrolling the TBODY in many different browsers at once. It's certainly worth a read - although I wouldn't have worked out how he did it if I didn't do a view-source and discover that he commented his style sheet fairly well. Really, who does that? :)
When you're playing with the styles, you should also note that I've explicitly set a background color on the 'noScroll' elements. When no background color is set on anything, then the background is rendered transparent - and when you scroll, you can watch the TBODY moving through underneath the THEAD.
Setting the 'noScroll' class on any of the table elements will fix it in place. You could do it to the first cell of every row to make a 'sticky' left column. Be careful here, because the ones outside the DIV will still render. Maybe a zIndex tweak will fix that, but I haven't tested it.
Donovan Marsh commented on how SELECT lists rendered wrong. This is a problem with all windowed objects that are rendered to a web page, of which the SELECT element is the most common. The problem here is that the drop down control is really that - the Win32 drop down combo, and not an internal rendering from IE. As a result, it's drawn last (that's my guess) and sits on top of everything else. So when you scroll down the table, if it contains a SELECT list, then it floats on top of your static THEAD. You would think that playing with the zIndex would help us here, but it doesn't - it's drawn last. The reference on zIndex on MSDN mentions that zIndex can not be applied to windowed controls. Ever noticed how you can't apply many styles to the select list either? Same reason. INPUT elements appear to work OK, however.
To solve this, there's three options. My preference is simple: don't use SELECT lists. Your next option, as points out in the comments, is to write some javascript that detects when the SELECT element is touching or under the THEAD, and set its visibility to hidden. No, I don't like it, but if you have to use SELECT lists, then you're sorta stuck. The third is to, of course, write your own DHTML drop down control (or get someone else's). This will work too, because then it's not windowed, but it's not a simple job.
WStoreyII asked how to make it happen to the footer. Well, without resorting to CSS expressions or javascript, I can only suggest absolute positioning. Here's an example:
<div class="container">
<table border="0" cellpadding="0" cellspacing="0" style="width: 384px">
<thead>
<tr class="noscroll">
<th>Col 1</th><th>Col 2</th><th>Col 3</th><th>Col 4</th><th>Col 5</th>
</tr>
</thead>
<tfoot>
<tr class="noscroll" style="position: absolute; top: 147px;">
<th>Col 1</th><th>Col 2</th><th>Col 3</th><th>Col 4</th><th>Col 5</th>
</tr>
</tfoot>
<tbody>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
</tbody>
</table>
</div>
[See this live]
I added a TFOOT and set an in-line style to position it in the right place. This is not nice, and the top has been carefully crafted to be in the right spot. What you'd really want is a once-off repositioning in script to make sure that it's correct for the current browsers rendering efforts - the top value needs to be the height of the scrollable DIV minus the height of the TFOOT element. In this case, my TFOOT renders at 13 pixels high.
In IE5, absolute positioning won't work. An expression to calculate the same sort of thing will likely work, however.
IE5 or non-strict
If you can't keep in strict compliance mode, or need to support IE5 as well, then it's time to fall back to the next option. CSS expressions.
Before you ask, I tried absolute positioning of the THEAD element while in non-strict mode, and it didn't work.
CSS expressions allow you put javascript within CSS. It's only supported in IE, but it's been there for a while now. You can reference pretty much any global client side JS variable, and you have access to the 'this' object too. Pretty cool. You can read about them on MSDN here.
The disadvantage of CSS expressions, however, is that they aren't calculated just once. Any time the window is resized or changed they are recalculated. It also happens anytime a javascript thread is finished - so if you track a lot of document events, then your expressions are continuously recalculated. If you catch the mousemove event - whoah! This last snippet of info I found in an article written by Erik Arvidsson. It's a good read about optimisation of expressions too!
Anyway, what we need to do is keep convincing IE that when it renders the table to keep the THEAD at a certain location. All we need to do in this case is make one little modification to the noscroll class in the above CSS.
.noscroll
{
position: relative;
top:expression(this.offsetParent.scrollTop);
background-color: white;
}
[See this live] - This sample works in IE5, IE6, and even IE7B2. In Firefox, it doesn't work at all.
Here we set the top style to be the same as the scrollTop of the offsetParent. This little piece of trickery works because of who the offsetParent actually is. Elements within a TABLE don't get the offsetParent set as what you'd expect - in the THEAD's case (and the TRs and TDs too) the offsetParent is actually the scrollable DIV that contains the TABLE, rather than the TABLE itself. Instead of forcing the THEAD to stay at the top of the TABLE, we force it to the top of the DIV.
In my blog comments nira pointed out how setting a background color on the scrollable DIV container to some color (say, yellow) made weird rendering bugs when you scrolled. She (I think nira is a she) is right too - you get this weird flicker thing going on. I found a solution for this problem - don't set a background color on the container DIV :) I found that setting it on the TBODY instead created the same effect but didn't render strangely.
Firefox
People who know me know I'm an IE developer, but since someone always asks, I might as well save them the hassle and cover it now.
I'm not a Firefox expert, and I can't get it to work to my liking, but here's the HTML I came up with. Feel free to fix it and post it back to me :)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>scrolling tbody</title>
<style type="text/css">
.tableScroll
{
border: 1px solid black;
width: 400px;
}
.container
{
height: 150px;
overflow: auto;
}
th
{
text-align: left;
}
</style>
</head>
<body>
<table class="tableScroll" border="0" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th>Col 1</th><th>Col 2</th><th>Col 3</th><th>Col 4</th><th>Col 5</th>
</tr>
</thead>
<tbody class="container">
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr>
</tbody>
</table>
</body>
</html>
[See this live] - Remember, this only works in Firefox. In IE, I honestly can't remember what happens. In IE7B2, it looks terrible.
It's fairly similar to the IE versions. The container class has been assigned the TBODY instead of the scrollable DIV (which is now gone). I removed the width and border styles from the container class and put them on the TABLE instead.
This works almost perfectly, except that the scroll bar gets rendered inside the table now, instead of inside the DIV. As a result, there isn't enough room (16 pixels missing for the scrollbar) to display the full TR, so a horizontal scrollbar appears too. Yick!
Opera [New addition as of 20050511]
Today I tested each of these methods in the latest version of Opera that was available for download (non-beta). None of them rendered the table with a scrollable tbody. The closest it came was to scroll the entire table in the IE6 version, but the THEAD scrolled up with the rest of the body. I'm not sure what to do to fix it yet, but I thought I'd post my findings so far :)
All at Once
To cover all browsers at the same time, you're in for interesting times. Go back and read again the article I mentioned before about the guy who got TBODY scrolling in multiple browsers. It's a lot of work but he has done the work for you. All I've really done is talked about my experiences, and split it out to get rid of all the non-IE specific stuff.
Enjoy.
Authors Note: When my blog transitioned over from dotnetjunkies.com to here, my articles didn't get copied over with their feedback - I had to do it by hand. This being the case, the article page still exists on the old host too (with it's original feedback) but the content of the article is just - just a link to here. For those of you who left feedback on the original article, my apologies, but I can't copy it across.
Note 2: No, doesn't exist anymore :) It's reaaalllly old now.