Smoothly Scrolling JavaScript Menus

Simple rectangular drop-down menus are common in web pages due to their ease of implementation. Smoothly scrolling/sliding non-rectangular menus are much rarer, but this document explains how to create them with little difficulty and gives example code.

Example of usage: Queensway Catholic Primary School website

Stacking menu components

In a smoothly scrolling/sliding menu the menu title and menu body (which contains the menu items) are contained in separate div elements, rather than one element that changes size. The menu title is displayed over the menu body, which scrolls/slides out smoothly from under it. In order to stop the menu body from showing when it is retracted, a ‘cover’ div (a rectangle of the background colour) is placed between the elements to precisely cover the menu body in its retracted position.

By default elements occuring later in the HTML code appear on top of elements that occur earlier, and this can be used to ensure that the menu title, body, and cover appear in the correct stacking order. If more advanced control is needed (because, for example, the menu is surrounded by other graphics), than the CSS z-index property can used to specify the stacking order.

Non-rectangular menus, anti-aliasing, and transparency

If the menu title or body are non-rectangular, their constituent graphics should be anti-aliased to avoid jagged edges. Ideally PNG alpha channels would be used to anti-alias against any background, but these are not supported by Internet Explorer 6. Instead the simple transparency of the GIF format will have to suffice.

If the base of the menu body has the same curve as the base of the menu title, then only the menu body should be anti-aliased, and the base of the menu title left aliased. When the menu is retracted the base of the menu body gives a smooth curve to the base of the menu title, and when the menu is open it avoids the problem of the base of the menu title being anti-aliased against the page background rather than then menu body background. This technique can be seen on the example website mentioned at the start of this document.

The HTML code

The menu consists of a combination of HTML, CSS, and JavaScript. The HTML code shown below can be placed anywhere in the document, as the elements are positioned absolutely using CSS, but remember that people using screen readers will hear the alt-text where it occurs in the source.

The outermost div contains all the menus. Extra menus can be added by making additional copies of the three inner divs, with the 0s in the ids and JavaScript calls changed into 1s, 2s, and so on.

The menu body is structured as an unordered list (the list formatting will be removed using CSS). The first li element contains an image to use at the top of the menu body — this can be used to create space which, in particular, is necessary if the menu title is curved. This is followed by a set of li elements containing the menu items — the example uses simple graphics, but these can easily be altered to use rollover effects. The final li element contains an image to use at the base of the menu body, which as mentioned above may also form the base of the menu title. In all cases the src attribute should be the name of the image file, the alt-text should be set to make the menu accessible to visitors who can’t see images, and the width and the height can be specified to avoid the page moving around while loading for visitors with slow connections.

The menu cover is just an empty div, which will be styled using CSS. The menu title contains an image to use as the title — as above, the src and alt-text should be set, and the width and height can also be specified.

<div class="menus">
  <div id="menu0body" onMouseOver="openMenu(0);" onMouseOut="closeMenu(0);">
    <ul>
      <li><img src="" alt=""></li> <-- the top of the menu body -->
      <li><a href=""><img src="" alt=""></a></li>
      <!-- and more menu items as necessary --> 
      <li><img src="" alt=""></li> <!-- the base of the menu body -->
    </ul>
  </div>
  <div id="menu0cover"></div>
  <div id="menu0title" onMouseOver="openMenu(0);" onMouseOut="closeMenu(0);"><img src="" alt=""></div>
  <!-- and more menus as necessary -->
</div>

The CSS code

The CSS code shown below should be placed in the head element of the document. Extra menus will require extra copies of the rules beginning #, with the 0s changed to 1s, 2s, and so on.

The first and second rules ensure the list is displayed as a stack of images — browsers differ in how they format lists and position images, which leads to seemingly irrelevant styles such as font-size:25%. The second rules saves space, as all the divs will be absolutely positioned.

The left and initial top positions of the menu body should be set in the #menu0body rule. The menu will initially be retracted, so the top position should be set to the top position of the menu title, plus the menu title height, minus the menu body height — if the advice above on anti-aliasing curves was followed, it might be necessary to add a few pixels on to this number.

In the #menu0cover rule, the left and top positions should be the same as those for the #menu0body rule. The width should equal that of the menu body, and the height should equal the difference between the top of the menu body and the top of the menu title. The background colour should be set to match the background of the document — if the document uses an image as the background, the combined background CSS property should be used.

The left and top positions of the menu title should be set in the #menu0title rule.

<style type="text/css">
  .menus img{
    border:0;
    vertical-align:bottom;
  }
  .menus ul{
    font-size:25%;
    list-style-type:none;
    padding:0;
    margin:0;
  }
  .menus div{
    position:absolute;
  }
  #menu0body{
    left:px;
    top:px;
  }
  #menu0cover{
    left:px;
    top:px;
    width:px;
    height:px;
    background-color:white;
  }
  #menu0title{
    left:px;
    top:px;
  }
</style>

The JavaScript code

The CSS code shown below should be placed in the head element of the document. It uses JavaScript’s setInterval method to update the menu position, avoiding the problems for users with slow connections caused by the fact that onLoad only activates once all the images are loaded.

The six arrays declared at the start of the code should each have length equal to the number of menus. These arrays are:

menuOpen
Whether a menu is open (or opening) or retracted; the items in this array should be set to false
menuDelay
Used to stop the menu twitching when the mouse pointer moves between the title and body; the items in this array should be set to 0
menuTop
The topmost position of the menu body; the items in this array should be the same as those set in the CSS
menuOffset
The current offset of the menu body; the items in this array should be set to 0
menuMaximum
The maximum offset of the menu body; the items in this array should be set so that the menu is fully visible when open (for curved menus this will be slightly less than the height of the menu body)
menuSpeed
The speed of the menu body; the items in this array should be set to zero

The scrollMenus function manages the movement of the menus — they smoothly accelerate and decelerate, even when the mouse pointer moves on and off as they open or close. On the example website mentioned at the start of this document this function has been extended to ensure that the menu bar is always positioned in the centre of the page, even if the browser is resized (this requires JavaScript as CSS does not allow for an element to have relative horizontal position and absolute vertical position).

<script type="text/javascript">
  menuOpen=[false];
  menuDelay=[0];
  menuTop=[0];
  menuOffset=[0];
  menuMaximum=[0];
  menuSpeed=[0];
  function openMenu(menu){
    menuOpen[menu]=true;
  }
  function closeMenu(menu){
    menuOpen[menu]=false;
    menuDelay[menu]=4;
  }
  function scrollMenus(){
    for (var i=0;i<menuOpen.length;i++){
      if (menuOpen[i] || menuDelay[i]>0 ){
        menuDelay[i]--;
        if (menuSpeed[i]<0){
          menuSpeed[i]++;
        }else{
          var stoppingDistance=menuSpeed[i]*(menuSpeed[i]+1)/2;
          if (menuMaximum[i]-menuOffset[i]<stoppingDistance) menuSpeed[i]--;
          if (menuMaximum[i]-menuOffset[i]>stoppingDistance+menuSpeed[i]+1){
            menuSpeed[i]++;
          }
        }
      }else{
        if (menuSpeed[i]>0){
          menuSpeed[i]--;
        }else{
          var stoppingDistance=menuSpeed[i]*(menuSpeed[i]-1)/2;
          if (menuOffset[i]<stoppingDistance) menuSpeed[i]++;
          if (menuOffset[i]>stoppingDistance-menuSpeed[i]+1){
            menuSpeed[i]--;
          }
        }
      }
      menuOffset[i]+=menuSpeed[i];
      document.getElementById('menu'+i+'body').style.top=
          menuTop[i]+menuOffset[i]+'px';
    }
  }
  window.setInterval('scrollMenus()',50);
</script>
This article was last edited on 3rd August 2011. The author can be contacted using the form below.
Back to home page
Bookmark with: