Practical Implementation of an "Intelligent" Store Navigation Bar
Published: 2017-12-16
Requirements and goals
On the main homepage of an e-commerce site, there is usually a prominent category navigation bar. As a key entry point for traffic distribution across the entire mall, the customer experience must feel natural and exceptional. Observant users may notice that on large sites like jd.com or tmall.com, when the mouse moves vertically within the primary navigation, the secondary menu responds with zero delay. Interestingly, when the user hovers over a primary menu item intending to click in its corresponding secondary menu area, the secondary menu does not switch even if the mouse briefly passes over other primary items. It feels like the menu “understands” you and can accurately anticipate your action. The lofty term is user behavior–based predictive switching; I call it an “intelligent” navigation bar. The effect is as follows.

Before we build it, let’s clarify the target behavior:
When the mouse normally switches between primary menu items, the secondary menu should respond with no delay;
When the mouse moves quickly toward the secondary submenu, the primary menu should not switch unnecessarily;
Background knowledge
Let’s outline the knowledge points we’ll need. If, while completing a small requirement like this, we can also fully understand the related topics, fill in knowledge gaps, and then transfer the same techniques to other scenarios by analogy, then the practice is thorough and valuable.
The difference between mouseenter and mouseover;
Use vector cross products to determine whether a point lies inside a triangle; (In this practice we choose approach 4: judge by whether cross-product signs are the same)
How to efficiently determine whether two numbers share the same sign;
HTML5 semantic tags – syntax and usage of dl, dt, and dd elements;
Among the items I’ve organized above, points 2, 5, and 6 are relatively simple and can be explained in a few sentences. Any one of the other three could justify a full article. I’ve attached my curated high-quality links; if some points are unclear, click through to learn more.
Hands-on walkthrough
I’ll take aprogressive enhancementapproach to the explanation. For the full sample code, seeCodePen.
Basic implementation
First, for the document structure, follow semantic principles. Useul li
for the primary menu on the left.
<ul>
<li data-id="a">
<span> Primary Nav 1 </span>
</li>
<li data-id="b">
<span> Primary Nav 2 </span>
</li>
···
</ul>
For the submenu on the right, usedl dt dd
tags, because they’re most suitable for a menu where one heading has several associated list items. For more details,click here.
<div id="sub" class="none">
<div id="a" class="sub_content none">
<dl>
<dt>
<a href="#"> Secondary Menu 1 </a>
</dt>
<dd>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
</dd>
</dl>
<dl>
<dt>
<a href="#"> Secondary Menu 1 </a>
</dt>
<dd>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
</dd>
</dl>
···
</div>
<div id="b" class="sub_content none">
<dl>
<dt>
<a href="#"> Secondary Menu 2 </a>
</dt>
<dd>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
<a href="#"> Tertiary Menu </a>
</dd>
</dl>
···
</div>
···
</div>
Next, add the JS interactions. By hovering over differentli
elements on the left, activate the display of different.sub_content
blocks on the right. Use the primary menu’sdata-id
attribute and the submenu’sid
value as the linkage hooks.
Here we need to choose whether to bindmouseenter
ormouseover
events. The difference can be summarized as:
When using mouseover/mouseout, the mouseover event fires when the pointer enters the bound element orenters any of its child elements,and will always trigger the mouseover event. If the mouse moves onto a child without leaving the bound element, the bound element’s mouseout also fires;
When using mouseenter/mouseleave,onlywhen the pointerenters the bound element(excluding when it enters any child),will it trigger the mouseenter event. If the mouse hasn’t left the bound element, moving over its children won’t trigger mouseleave;
To aid understanding, I made a demo, please seemouseenter/mouseover.
By comparison, it’s clear we only need to bindli
on eachmouseenter
/mouseout
event.
var sub = $("#sub"); // Wrapper for the submenu
var activeRow, // Active primary menu item
activeMenu; // Active submenu
$("#wrap").on("mouseenter", function() {
// Show submenu
sub.removeClass("none");
})
.on("mouseleave", function() {
// Hide submenu
sub.addClass("none");
// Reset the two active variables
if (activeRow) {
activeRow.removeClass("active");
activeRow = null;
}
if (activeMenu) {
activeMenu.addClass("none");
activeMenu = null;
}
})
.on("mouseenter", "li", function(e) {
if (!activeRow) {
activeRow = $(e.target).addClass("active");
activeMenu = $("#" + activeRow.data("id"));
activeMenu.removeClass("none");
return;
}
// If a menu is already active, reset it first
activeRow.removeClass("active");
activeMenu.addClass("none");
activeRow = $(e.target);
activeRow.addClass("active");
activeMenu = $("#" + activeRow.data("id"));
activeMenu.removeClass("none");
});
This achieves the basic effect. Note the use ofevent delegation, which is a good practice for DOM performance optimization and keeps the code elegant.
However, this version has usability issues. To select a submenu, users must carefully keep the pointer within the currently selected primary menu and move it along a zigzag path to the submenu before they can continue, as shown below.

Clearly, users want to move the pointer along the shortest diagonal path when selecting a submenu under a primary item, without inadvertently activating other primary items they pass over. Let’s improve this.
Solve the diagonal movement problem
The core issue is that mouseenter fires frequently on each primary item as the mouse moves. Naturally, we consider delaying execution, and we introduce debounce/throttle to avoid repeated triggers. Each time a primary item is triggered, we don’t immediately show its submenu; instead, we defer by 300ms. After the last trigger and 300ms have passed, we check whether the pointer is within the submenu region. If it is, we simply return without switching menus, as below.
.on("mouseenter", "li", function(e) {
if (!activeRow) {
active(e.target); // A function that activates the corresponding submenu
return;
}
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function() {
if (mouseInSub) {
return;
}
activeRow.removeClass("active");
activeMenu.addClass("none");
active(e.target);
timer = null;
}, 300);
});
As a result, every switch between primary items incurs a 300ms delay. So when users move up and down within the primary menu area, or actually want to switch quickly, this crude delay solves the diagonal movement issue but introduces a new problem, as shown below.

How can we make the submenu respond quickly when the user truly intends to switch primary items rapidly, yet apply the delayed trigger only when the user intends to select the submenu, thereby allowing diagonal movement? If your knowledge is limited to programming or computer science, solving this is indeed tricky. Here we need somecross-disciplinary, heuristic thinking: abstract a mathematical model from user behavior to predict menu switching.
Further improvement
In fact, we can abstract a triangle from the mouse movement trajectory (as shown below). Its three points are: the top-left corner (top) of the submenu container, its bottom-left corner (bottom), and the point the pointer just passed through (pre). The point cur inside the triangle represents the pointer’s current position. The distance between pre and cur depends on the granularity of the mousemove event; it’s usually very short. For illustration, the diagram is suitably scaled up.

What’s the significance of this triangle? In typical user behavior, we can assume that when the pointer is inside the triangle, the user tends to select the submenu; when it’s outside, the user is more inclined to switch primary items quickly. As the user keeps moving the mouse, many such triangles are formed continuously. The breakthrough becomes:Continuously listen to the pointer position and determine whether the current point lies within the triangle formed by the last point and the submenu’s top-left and bottom-left corners.
Listening continuously to the pointer position can be done easily with mousemove. Be mindful of when to bind and unbind it so it only fires within the menu area, since continuous listening and firing isn’t cheap for the browser. To determine whether a point lies inside a triangle, we’ll use item 4 from the background knowledge: decide by whether the vector cross products have the same sign. The mathematical proof is out of scope here; it suffices to know the result is rigorous.
Next, we’ll implement vectors and their cross product in code:
// A vector is the end point minus the start point
function vector(a, b) {
return {
x: b.x - a.x,
y: b.y - a.y
}
}
// Vector cross product
function vectorPro(v1, v2) {
return v1.x * v2.y - v1.y * v2.x;
}java
Then we’ll use the two helper functions above to determine whether a point lies inside a given triangle. The function takes four known points and returns whether the signs of the three cross products are pairwise the same. If they are, the point is inside; otherwise, it’s outside.
// Determine whether a point is inside a triangle
function isPointInTranjgle(p, a, b, c) {
var pa = vector(p, a);
var pb = vector(p, b);
var pc = vector(p, c);
var t1 = vectorPro(pa, pb);
var t2 = vectorPro(pb, pc);
var t3 = vectorPro(pc, pa);
return sameSign(t1, t2) && sameSign(t2, t3);
}
// Efficiently check same sign using bitwise ops
function sameSign(a, b) {
return (a ^ b) >= 0;
}
Note thesameSign
helper that checks whether two values have the same sign. There are many ways to do this, but here we cleverly use the highest bit—the sign bit—in binary. We applybitwise XORto the two values: different sign bits yield 1, same yield 0. So if the final sign bit is 1 (i.e., the result is negative), the values have different signs; otherwise, they’re the same.Bitwise operations are more efficient than operating on non-binary numbers directly, so in this scenario of frequent sign checks, they’re very helpful for performance optimization.
Finally, using the helper functions above, we track mouse position to decide whether to enable the timer, and selectively apply the previous section’s optimization. This achieves the final requirement. (Complete sample codeCodePen)
// Whether a delay is needed
function needDelay(ele, curMouse, prevMouse) {
if (!curMouse || !prevMouse) {
return;
}
var offset = ele.offset(); // offset() returns/sets the element’s position relative to the document
// Top-left point
var topleft = {
x: offset.left,
y: offset.top
};
// Bottom-left point
var leftbottom = {
x: offset.left,
y: offset.top + ele.height()
};
return isPointInTranjgle(curMouse, prevMouse, topleft, leftbottom);
}
Takeaways
From this exercise, my biggest takeaway is theproductivity-boosting value of advanced mathematics, haha...
Pardon my shallowness, but I still feel the excitement from when I first saw this example. Recently I skimmed a classic textbook in deep learning—more than half of it covers the math involved. It amazed me: this is how math is used. What a pity I didn’t appreciate it earlier...
Looking at problems from a dominant height and broad perspective, can turn the unsolvable into solvable, and the unique solution into multiple solutions. That’s what a true expert looks like to me.
If this article gives you inspiration for coding itself, or for applying vectors (math) to similar scenarios involving points, lines, and planes, or even sparks thoughts about education and guidance, then I’ve achieved my goal in writing it.
Last updated