AboutBlogContact
Backend EngineeringJune 30, 2001 8 min read 138Updated: June 22, 2026

DHTML: Building Dynamic Dropdown Menus with JavaScript and CSS (2001)

AunimedaAunimeda
📋 Table of Contents

By 2001, JavaScript had become powerful enough to manipulate HTML elements in real time - changing their styles, positions, and visibility without a page reload. The technology had a name: DHTML, or Dynamic HTML. It was not a new language or a specification; it was a marketing term for the combination of JavaScript, CSS, and the Document Object Model used to create interactive effects.

The killer application of DHTML in 2001 was the dropdown navigation menu. Every corporate website wanted one. Hover over "Products," a submenu drops down showing subcategories. Hover over "Services," a different submenu appears. No page reload. No Flash. Pure HTML, CSS, and JavaScript.

Building it to work in both Internet Explorer 5 and Netscape 6 was a genuine engineering challenge.


The Browser Compatibility Problem

IE5 and Netscape 6 both supported the W3C DOM (document.getElementById, element.style), but they differed in how events bubbled and how onmouseover worked across nested elements. The earlier Netscape 4 used a completely different document.layers model that we will ignore - by 2001, Netscape 4's market share had fallen enough to treat it as a known limitation.

The event model problem:

// IE5: onmouseover/onmouseout fired on the element AND all child elements
// Netscape 6: same behavior - W3C bubbling model
// But the specific event object properties differed:

// IE5 event model:
element.onmouseover = function() {
    var target = window.event.srcElement;   // IE-specific
    var relatedTarget = window.event.fromElement;
};

// Netscape/W3C event model:
element.onmouseover = function(e) {
    var target = e.target;            // W3C standard
    var relatedTarget = e.relatedTarget;
};

// Cross-browser wrapper - we all had this:
function getEvent(e) {
    return e || window.event;
}
function getTarget(e) {
    return e.target || e.srcElement;
}
function getRelatedTarget(e) {
    return e.relatedTarget || e.fromElement;
}

The Complete Dropdown Menu Implementation

The approach: absolutely-positioned <div> elements hidden by default, shown on onmouseover, hidden on onmouseout. The trick was handling the mouse moving from the parent link into the submenu - this fired onmouseout on the parent, which would close the menu before the user could click anything.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>DHTML Navigation Menu - 2001</title>
<style type="text/css">
/* Reset - browsers in 2001 had wildly different default margins */
body, td, th, p { font-family: Arial, Helvetica, sans-serif; font-size: 13px; }

/* Top-level navigation bar */
#navbar {
    width: 100%;
    background-color: #336699;
    height: 28px;
    position: relative;   /* needed for absolute child positioning to work */
}

/* Each top-level nav item - positioned inline */
.nav-item {
    position: relative;
    display: inline;
    float: left;
}

.nav-item a {
    display: block;
    float: left;
    padding: 5px 14px;
    color: #ffffff;
    text-decoration: none;
    font-weight: bold;
    font-size: 12px;
}

.nav-item a:hover {
    background-color: #4477aa;
}

/* Dropdown submenu container - hidden by default */
.submenu {
    position: absolute;
    top: 28px;       /* height of navbar */
    left: 0;
    width: 160px;
    background-color: #f5f5f5;
    border: 1px solid #336699;
    border-top: 2px solid #336699;
    display: none;   /* hidden until mouseover */
    z-index: 100;    /* above page content */
}

/* Submenu links */
.submenu a {
    display: block;
    padding: 5px 10px;
    color: #333333;
    text-decoration: none;
    border-bottom: 1px solid #dddddd;
    font-weight: normal;
}

.submenu a:hover {
    background-color: #d0e0f0;
    color: #003366;
}
</style>
</head>
<body>

<div id="navbar">
  <!-- Each nav item has an id for JavaScript targeting -->
  <div class="nav-item" id="nav-products">
    <a href="products.html"
       onmouseover="showMenu('menu-products', 'nav-products')"
       onmouseout="scheduleHide('menu-products')">
      Products &#9660;
    </a>
    <div class="submenu" id="menu-products"
         onmouseover="cancelHide('menu-products')"
         onmouseout="scheduleHide('menu-products')">
      <a href="products/software.html">Software</a>
      <a href="products/hardware.html">Hardware</a>
      <a href="products/services.html">Services</a>
      <a href="products/support.html">Support</a>
    </div>
  </div>

  <div class="nav-item" id="nav-about">
    <a href="about.html"
       onmouseover="showMenu('menu-about', 'nav-about')"
       onmouseout="scheduleHide('menu-about')">
      About Us &#9660;
    </a>
    <div class="submenu" id="menu-about"
         onmouseover="cancelHide('menu-about')"
         onmouseout="scheduleHide('menu-about')">
      <a href="about/company.html">Company</a>
      <a href="about/team.html">Our Team</a>
      <a href="about/careers.html">Careers</a>
    </div>
  </div>

  <div class="nav-item" id="nav-contact">
    <a href="contact.html"
       onmouseover="showMenu('menu-contact', 'nav-contact')"
       onmouseout="scheduleHide('menu-contact')">
      Contact &#9660;
    </a>
    <div class="submenu" id="menu-contact"
         onmouseover="cancelHide('menu-contact')"
         onmouseout="scheduleHide('menu-contact')">
      <a href="contact/office.html">Our Office</a>
      <a href="contact/form.html">Send Message</a>
    </div>
  </div>
</div>

<script type="text/javascript">
// DHTML dropdown menu logic - 2001 style
// Requires: IE5+, Netscape 6+, Opera 5+

var hideTimers = {};   // Track delayed-hide timers per menu

// Show a submenu, close all others
function showMenu(menuId, navItemId) {
    // Close any other open menu first
    hideAllMenus();
    
    var menu = document.getElementById(menuId);
    if (menu) {
        menu.style.display = 'block';
    }
    
    // Cancel any pending hide for this menu
    cancelHide(menuId);
}

// Schedule a menu to hide - delayed so mouse can move into submenu
function scheduleHide(menuId) {
    // Cancel any existing timer for this menu
    if (hideTimers[menuId]) {
        clearTimeout(hideTimers[menuId]);
    }
    // Hide after 300ms - enough time to move mouse into submenu
    hideTimers[menuId] = setTimeout(function() {
        hideMenu(menuId);
    }, 300);
}

// Cancel a scheduled hide (mouse moved into submenu)
function cancelHide(menuId) {
    if (hideTimers[menuId]) {
        clearTimeout(hideTimers[menuId]);
        hideTimers[menuId] = null;
    }
}

function hideMenu(menuId) {
    var menu = document.getElementById(menuId);
    if (menu) {
        menu.style.display = 'none';
    }
    hideTimers[menuId] = null;
}

function hideAllMenus() {
    var menus = document.getElementsByTagName('div');
    for (var i = 0; i < menus.length; i++) {
        if (menus[i].className === 'submenu') {
            menus[i].style.display = 'none';
        }
    }
}

// Close all menus if user clicks anywhere on the page
document.onclick = function() {
    hideAllMenus();
};
</script>

</body>
</html>

The setTimeout Trick

The most important technique in that implementation is the 300ms delayed hide. Without it:

  1. User hovers over "Products" → submenu appears
  2. User moves mouse from "Products" link toward submenu
  3. onmouseout fires on "Products" link immediately
  4. Submenu disappears before the user reaches it

The fix: don't hide immediately. Schedule the hide with setTimeout. When the mouse enters the submenu, call cancelHide to clear the timer. The submenu stays open as long as the mouse is anywhere within it.

// Why 300ms? Empirical testing in 2001.
// 100ms: too fast - slow mice / trackballs still lost the menu
// 500ms: menus felt "sticky" - lingered after intentional exit
// 300ms: right balance on the hardware common in 2001
// (the same heuristic appears in modern tooltip libraries today)

The z-index War

In 2001, z-index behaved inconsistently across browsers. IE5 had a notorious bug: <select> elements (HTML dropdowns) always rendered on top of absolutely-positioned elements, regardless of z-index. Your beautiful DHTML menu would drop down behind a <select> on the page.

The fix: place an <iframe> behind your menu, sized to cover it. The iframe created a new stacking context that covered <select> elements.

// IE5/IE6 select-overlap fix - the "iframe shim"
// Required on any page that had <select> elements near the nav

function createIframeShim(menuElement) {
    var shim = document.createElement('iframe');
    shim.src = 'about:blank';
    shim.style.position  = 'absolute';
    shim.style.border    = '0';
    shim.style.filter    = 'alpha(opacity=0)';   // IE-only transparency
    shim.style.zIndex    = '99';    // one below the menu
    shim.frameBorder     = '0';
    
    // Size the shim to match the menu
    shim.style.width  = menuElement.offsetWidth  + 'px';
    shim.style.height = menuElement.offsetHeight + 'px';
    
    // Insert before the menu in DOM
    menuElement.parentNode.insertBefore(shim, menuElement);
    menuElement.style.zIndex = '100';
    
    return shim;
}

This was the ugly reality of DHTML development in 2001. Every clever technique had an IE-specific workaround.


Hit Counter and Page Statistics Using DHTML

Another DHTML technique from 2001: updating a visible page counter without reloading. This used document.write() inline - the precursor to Ajax, and equally crude:

<!-- Embed live visitor count from a CGI script -->
<!-- The script outputs JavaScript that calls document.write() -->
<script type="text/javascript" src="/cgi-bin/counter.pl?output=js"></script>

<!-- counter.pl outputs something like: -->
<!--   Content-Type: text/javascript                         -->
<!--   document.write("You are visitor number <b>14,847</b>"); -->

This technique - loading a <script> tag from an external URL that outputs JavaScript - was used for hit counters, ad networks, web rings, and third-party analytics. It is essentially the same mechanism as modern third-party tracking scripts. The pattern from 2001 became the foundation of the entire ad tech industry.


What DHTML Became

By 2005, jQuery had abstracted the browser incompatibilities that made DHTML development painful. The setTimeout approach to preventing premature menu close is still used in virtually every modern tooltip and dropdown component. The z-index stacking context problem was solved by CSS specification improvements in browsers circa 2007.

The iframe shim technique for <select> overlap disappeared when IE7 fixed the bug in 2006. The detection-and-branch pattern (if (window.event) vs if (e.target)) disappeared when IE9 adopted the W3C event model in 2011.

What remained: the conceptual model of JavaScript manipulating CSS properties and element visibility to create interactive behavior without server round-trips. That model is still what every modern UI framework - React, Vue, Angular - implements at its core. The dropdown menu of 2001 and a React popover of today are architecturally the same thing, separated by twenty years of tooling improvement.


Aunimeda builds production-grade backend systems - APIs, microservices, real-time applications, and system integrations.

Contact us for backend engineering services. See also: Custom Software Development, Web Development

Read Also

Why We Chose Node.js in 2011aunimeda
Backend Engineering

Why We Chose Node.js in 2011

In 2011 Node.js was 2 years old, had no LTS, and most PHP developers thought it was a toy. We moved a real-time dashboard to it anyway. Here's the exact reasoning, the risks we accepted, and what happened.

The Node.js Revolution (2011): How JavaScript Conquered the Serveraunimeda
Backend Engineering

The Node.js Revolution (2011): How JavaScript Conquered the Server

Ryan Dahl showed Node.js at JSConf 2009. By 2011 we were running it in production. Non-blocking I/O, npm, and the realization that one language could run everywhere changed how we hired, how we built, and how we thought.

Node.js: Ryan Dahl's 45-Minute Talk That Rewrote Backend Development (2009)aunimeda
Backend Engineering

Node.js: Ryan Dahl's 45-Minute Talk That Rewrote Backend Development (2009)

On November 8, 2009, Ryan Dahl presented Node.js at JSConf EU in Berlin. He showed JavaScript running on the server with non-blocking I/O. The audience sat in silence, then gave a standing ovation. Within three years, Node.js had more packages than any other programming language ecosystem. This is what he built and why it worked.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles