Web Developer

Scrolling Layers

Scrolling is a basic User Interface function that has been present since the inception of UIs.  In essence, when the content of any given document is larger than a single "screenful", scrolling makes the portion of the document not currently available on the screen accessible.  

Browsers are generally good about providing scrollbars when they are necessary for longer pages.  That's a two-edged sword though.  For the most part, users don't want to scroll and generally don't.  They'll typically scan the current screen for content and if the content isn't present immediately they'll go elsewhere.

By taking the scrolling ability away from the browser, you gain the ability to grab the user's attention with part of your UI.  In other words, you can present your entire document in a custom scrolling area without relying on the browser to provide the scrolling capabilities.  This can lead to some interesting UI designs as well as enhance the overall look and feel for your site.

Designing a scrolling system is actually pretty painless.  Since we know how to manipulate the top property of any layer, all we'll need to do is assign a new value to that property to achieve the effect of scrolling.  The only thing to be wary of is that the mathematics for scrolling layers is counterintuitive.  To get the scrolling up effect, you'll actually want to move the top of the layer further down the screen.  This translates into adding some value to the top property.  The opposite is true of scrolling down: move the layer up on the screen by subtracting some value from the top property.

Let's look at some code.  Here's an example of basic scrolling which we'll analyze in just a moment:

<html>
<head>
<title>Basic Scrolling</title>
<script language="JavaScript">
if(document.getElementById){
doc = 'document.getElementById("';
sty = '").style';
htm = '';
}
if(document.layers){
doc = 'document.';
sty = '';
htm = '.document'
}
if(document.all){
doc = 'document.all.';
sty = '.style';
htm = '';
}
var selected = 0;
var currentTop = 100;
var timer=null;
function scrollUp(){
currentTop+=5;
eval(doc + 'text' + selected + sty).top = currentTop;
timer = setTimeout('scrollUp()',100);
}
function scrollDown(){
currentTop-=5;
eval(doc + 'text' + selected + sty).top = currentTop;
timer = setTimeout('scrollDown()',100);
}
function stopScroll(){
clearTimeout(timer);
}
function showText(index){
eval(doc + 'text' + selected + sty).visibility = 'hidden';
eval(doc + 'text' + index + sty).visibility = 'visible';
eval(doc + 'text' + index + sty).top = 100;
selected = index;
currentTop = 100;
}
</script>
<style>
.text{
position: absolute;
top: 100;
left: 130;
color: rgb(0,0,0);
visibility: hidden;
z-index: 2;
}
</style>
</head>
<body>
<div style="position:absolute; left: 10px; top: 150px;">
<a href="#" onMouseOver="scrollUp()" onMouseOut="stopScroll()">
Scroll Up</a>
</div>
<div style="position:absolute; left: 10px; top: 175px;">
<a href="#" onMouseOver="scrollDown()" onMouseOut="stopScroll()">
Scroll Down</a>
</div>
<div style="position: absolute; width: 100; top: 10;">
<p><a href="javascript:showText(1)">Text1</a></p>
<p><a href="javascript:showText(2)">Text2</a></p>
<p><a href="javascript:showText(3)">Text3</a></p>
</div>
<div id="text0" class="text" style="visibility: visible;">
<p>Text0</p>
</div>
<div id="text1" class="text">
Text1
</div>
<div id="text2" class="text">
Text2
</div>
<div id="text3" class="text">
Text3
</div>
</body>
</html>

Let's start with the style sheet.  Here we declare a simple rule that positions text layers.  We'll assign this class to any layer that will be scrolled.

Let's move on to the HTML part of the page.  Here there are really three sections: a section that toggles the visibility of the three text layers, a section that represents are scrolling controls, and the scrolling layers themselves.  You'll notice that we'll be using mouseOver and mouseOut events to control our scrolling rather than clicks.  It makes it easier to use the scrolling system this way and, let's face it, it's just plain cooler!

That does bring us to an interesting UI design question though.  If we examine the intricacies of conventional scrollbars, we'll see that their design is usually lacking.  Dr. Alan Cooper, the so-called father of Visual Basic, has remarked about the deficiencies inherent in scrollbar design for years.  Dr. Cooper has taken note of the fact that scrollbars require the use of two different types of muscle control: large and small.  Essentially, he notes that to scroll up, users need to click on the up button at the top of the scrollbar: a small muscle movement.  To then scroll down, users must move the mouse to the down button of the scrollbar which is usually at the other end of the screen: a large muscle movement.  Then, to actually scroll, users must again engage their small muscle control and click the button.  If you have the time and inclination, I recommend Cooper's book, About Face: The Essentials of User Interface Design, as a good read.  We'll see if we can alleviate some of Dr. Cooper's concerns!

The last part of our code to examine is the script where all of the work is done.  After masking the browser, three variables are declared: one to keep track of the currently selected layer, one to keep track of the top property for the layers, and one to control the timer used for scrolling.  I'll skip over the showText() function as I assume you are familiar with it.  Just note that whenever a text layer becomes visible, its top property is set to 100 - the original position and that currentTop is also reset back to 100 - the original value.  This allows us to get each layer to show up in the same location every time it becomes visible.

The scrollUp() and scrollDown() functions are almost identical:

function scrollUp(){
currentTop+=5;
eval(doc + 'text' + selected + sty).top = currentTop;
timer = setTimeout('scrollUp()',100);
}
function scrollDown(){
currentTop-=5;
eval(doc + 'text' + selected + sty).top = currentTop;
timer = setTimeout('scrollDown()',100);
}

The only difference is that when we are scrolling up we are adding 5 pixels to the top and when we are scrolling down we are subtracting 5 pixels from the top.  We then make a recursive call to keep the layer scrolling.  This recursive call is a bit interesting as we now assign the return value from the setTimeout() function to the timer variable.  This allows us to stop the scrolling in the stopScroll() function:

function stopScroll(){
clearTimeout(timer);
}

That simple call to clearTimeout ends the recursive call.  Cool!  Take a closer look at the events and how they are attached to the scrolling controls:

<div style="position:absolute; left: 10px; top: 150px;">
<a href="#" onMouseOver="scrollUp()" onMouseOut="stopScroll()">
Scroll Up</a>
</div>
<div style="position:absolute; left: 10px; top: 175px;">
<a href="#" onMouseOver="scrollDown()" onMouseOut="stopScroll()">
Scroll Down</a>
</div>

Remember, we have to wrap up the controls in <a> elements if we want to avoid Netscape 4's event handler model.  For this simple example that seemed the best thing to do.  You can view the example here.

A Better Example

For some very obvious reasons, the above example needs some improvement.  While we did take care of some of Dr. Cooper's concerns by positioning our controls close to each other, we haven't done anything about keeping the content on a single screen.  In fact, our implementation just mimics what the browser's scrollbar already does!

In order to limit the content to the current screen we'll have to investigate the concept of clipping regions.  The good news is that establishing clipping regions for our layers is relatively simple.  The bad news is that manipulating the regions from inside our scripts can be cumbersome.  Well, we've got to play the hand we're dealt so let's have at it!

A clipping region is nothing more than a view port into some underlying area.  If you understand the notion of spotlights, you can understand clipping regions.  Think of an actor standing on a stage illuminated by a single spotlight.  The only visible part of the stage is the part that's lit by the spotlight.  The rest of the stage remains unchanged - only you can't see it.  You only see what is inside the bounds of the spotlight.  If the light moves, different parts of the stage become visible while the visible parts are then hidden.

Clipping regions are identical to spotlights.  They represent the viewable region of any layer.  In theory, the clipping region can be any shape but, in practice, only the rectangle shape is supported.  You can set the clipping region of any layer inside a style sheet declaration by using the clip property.  By default, the clipping region for a given layer is the entire layer but you are free to manipulate that.  A typical clip property looks something like this:

clip: rect(0,400,300,0);

The rect portion is self explanatory.  The numbers follow the same convention that we've seen in the margin and padding properties: top, right, bottom, left.  The above declaration says to create a rectangle whose dimensions are 400 pixels wide by 300 tall (the 400 and 300).  It also says to set the rectangle in the top left corner of the layer (the two zeros).  Finally, it says that only the content within that rectangle should be visible - regardless of how large the layer is:

With regards to our scrolling layers, the clipping region will move with the top property.  In other words, the region is fixed at 0,0.  If the layer scrolls up, so does the clipping region.  If we want to keep the clipping region fixed on the screen, we'll have to modify it.  In general, when the top property increases, the clipping region's top and bottom should decrease and vice-versa.  So, when the layer scrolls up, the clipping region moves down; when the layer scrolls down, the clipping region moves up.

Let's see if we can translate the above pseudo-code into a working example.  Here's the code to analyze:

<html>
<head>
<title>Scrolling</title>
<script language="JavaScript">
var selected = 0;
var currentTop = 100;
//these values from clip region in style
var clipTop = 0;
var clipRight = 400;
var clipBottom = 300;
var clipLeft = 0;
if(document.getElementById){
doc = 'document.getElementById("';
sty = '").style';
htm = '';
}
if(document.layers){
doc = 'document.';
sty = '';
htm = '.document'
}

if(document.all){
doc = 'document.all.';
sty = '.style';
htm = '';
}
var timer=null;
function scrollUp(){
currentTop+=5;
clipTop-=5;
clipBottom-=5;
eval(doc + 'text' + selected + sty).top = currentTop;
if(document.all||document.getElementById)
eval(doc + 'text' + selected + sty).clip = 'rect(' + clipTop + ' ' + clipRight + ' ' + clipBottom + ' ' + clipLeft + ')';
if(document.layers){
eval(doc + 'text' + selected + sty).clip.top = clipTop;
eval(doc + 'text' + selected + sty).clip.bottom = clipBottom;
}
timer = setTimeout('scrollUp()',100);
}

function scrollDown(){
currentTop-=5;
clipTop+=5;
clipBottom+=5;
eval(doc + 'text' + selected + sty).top = currentTop;
if(document.all||document.getElementById)
eval(doc + 'text' + selected + sty).clip = 'rect(' + clipTop + ' ' + clipRight + ' ' + clipBottom + ' ' + clipLeft + ')';
if(document.layers){
eval(doc + 'text' + selected + sty).clip.top = clipTop;
eval(doc + 'text' + selected + sty).clip.bottom = clipBottom;
}
timer = setTimeout('scrollDown()',100);

}

function stopScroll(){
clearTimeout(timer);
}
function showText(index){
eval(doc + 'text' + selected + sty).visibility = 'hidden';
eval(doc + 'text' + index + sty).visibility = 'visible';
eval(doc + 'text' + index + sty).top = 100;
selected = index;
currentTop = 100;
clipTop = 0;
clipRight = 400;
clipBottom = 300;
clipLeft = 0;
}
</script>
<style>
.text{
position: absolute;
top: 100;
left: 130;
width: 400px;
color: rgb(0,0,0);
clip: rect(0,400,300,0);
visibility: hidden;
}
.bg{
position: absolute;
top: 90;
left: 120;
width: 420px;
height:500;
clip: rect(0,420,320,0);
background-color: rgb(200,150,150);
}
</style>
</head>
<body>
<div style="position:absolute; left: 540px; top: 100px;">
<a href="#" onMouseOver="javascript:scrollUp()" onMouseOut="stopScroll()">
Scroll Up</a>
</div>
<div style="position:absolute; left: 540px; top: 122px;">
<a href="#" onMouseOver="javascript:scrollDown()" onMouseOut="stopScroll()">
Scroll Down</a>
</div>
<div style="position: relative; width: 100;">
<p><a href="javascript:showText(1)">Text1</a></p>
<p><a href="javascript:showText(2)">Text2</a></p>
<p><a href="javascript:showText(3)">Text3</a></p>
</div>
<div class="bg">
</div>
<div id="text0" class="text" style="visibility: visible;">
<p>Text0</p>
</div>
<div id="text1" class="text">
Text1
</div>
<div id="text2" class="text">
Text2
</div>
<div id="text3" class="text">
Text3
</div>
</body>
</html>

Let's start again in the style sheet.  This time, I've included a clip property for the text rule as well as a rule for a background for the text.  Just to prove a point, I set a clip property for the bg rule as well. So even though the height is set to 500 pixels, only the first 320 pixels will be visible:

.text{
position: absolute;
top: 100;
left: 130;
width: 400px;
color: rgb(0,0,0);
clip: rect(0,400,300,0);
visibility: hidden;
}
.bg{
position: absolute;
top: 90;
left: 120;
width: 420px;
height:500;
clip: rect(0,420,320,0);
background-color: rgb(200,150,150);
}

The HTML part of the page is largely unchanged other than moving the scrolling controls over to the top right corner of the text layers:

<div style="position:absolute; left: 540px; top: 100px;">
<a href="#" onMouseOver="javascript:scrollUp()" onMouseOut="stopScroll()">
Scroll Up</a>
</div>
<div style="position:absolute; left: 540px; top: 122px;">
<a href="#" onMouseOver="javascript:scrollDown()" onMouseOut="stopScroll()">
Scroll Down</a>
</div>

The interesting stuff happens once again in the script.  We've added some global variables to keep track of the clipping region's properties which are initialized to the values in the text style rule.  Even though we'll really only manipulate the top and bottom properties, we'll need to track the others as you'll soon see:

var clipTop = 0;
var clipRight = 400;
var clipBottom = 300;
var clipLeft = 0;

The showText() function is largely unchanged except for the addition of re-setting the clipping variables:

function showText(index){
eval(doc + 'text' + selected + sty).visibility = 'hidden';
eval(doc + 'text' + index + sty).visibility = 'visible';
eval(doc + 'text' + index + sty).top = 100;
selected = index;
currentTop = 100;
clipTop = 0;
clipRight = 400;
clipBottom = 300;
clipLeft = 0;
}

Let's take a closer look at the new scrollUp() function.  The scrollDown() function is identical apart from direction.

function scrollUp(){
currentTop+=5;
clipTop-=5;
clipBottom-=5;
eval(doc + 'text' + selected + sty).top = currentTop;
if(document.all||document.getElementById)
eval(doc + 'text' + selected + sty).clip = 'rect(' + clipTop + ' ' + clipRight + ' ' + clipBottom + ' ' + clipLeft + ')';
if(document.layers){
eval(doc + 'text' + selected + sty).clip.top = clipTop;
eval(doc + 'text' + selected + sty).clip.bottom = clipBottom;
}
timer = setTimeout('scrollUp()',100);
}

The scrollUp() function actually performs two functions for us.  We first increase the currentTop variable and decrease the clipTop and clipBottom variables by the same 5 pixels.  We then set the currently visible layer's top property using currentTop and the clipping region's boundaries using the new values for clipTop and clipBottom.  Once again, Netscape 4 does things a little differently but, interestingly enough, their approach is marginally easier (a first!).  IE and Netscape 6 require you to build a string for the new clip region's boundaries (that's why we saved all four values at the start).  Netscape 4, on the other hand, breaks the boundaries into separate properties with associated values.  In any event, we're forced to fork our code.  You can view the example here.  

You can also view a graphically enhanced version here that includes the added feature of stopping the scrollUp function when the layer hits the original starting point of 100 pixels.  Check the scrollUp() function to see how easy that is to implement.