EN

United States (EN)

Australia (EN)

Canada (EN)

Canada (FR)

France (FR)

Germany (DE)

Ireland (EN)

Netherlands (NL)

Spain (ES)

United Kingdom (EN)

EN

United States (EN)

Australia (EN)

Canada (EN)

Canada (FR)

France (FR)

Germany (DE)

Ireland (EN)

Netherlands (NL)

Spain (ES)

United Kingdom (EN)

Blog

No more menu rage: Smooth navigation with useSafeArea

Author

Published

October 23, 2025

Read time

8 MIN

Graphic illustration of a laptop connected to circuits

For Jasmeet Singh,  Software Engineer II on Rippling's Web Design Systems team — a common user frustration became clear. Navigating multi-level menus often led to accidental closures. Manual fixes were time-consuming and prone to error, and existing solutions didn't address the specific nuances of diagonal mouse movement. Recognizing the gap, he set out to build a tailored solution. Discover Jasmeet’s insights on creating useSafeArea and how it now ensures seamless menu interactions for Rippling users.


Have you ever tried to navigate a dropdown menu with submenus, only to have the submenu disappear the moment your mouse cursor strayed slightly off the intended path? If you've used any modern web application with hierarchical navigation, you've likely experienced this frustrating UX problem. Today, I want to share my journey of solving this challenge by implementing a custom React hook called useSafeArea.

What are nested menus?

Nested menus are hierarchical user interface components that reveal additional menu items when hovering over or clicking a parent item. Think of the classic dropdown menus you see in:

  • Navigation bars with category submenus

  • Context menus in applications like file explorers

  • Settings panels with nested configuration options

  • Multi-level dropdown selectors

No More Menu Rage slider 01
No More Menu Rage slider 02
No More Menu Rage slider 03
No More Menu Rage slider 04

1/4

These menus are ubiquitous in modern web applications because they provide an efficient way to organize complex hierarchical information without cluttering the interface

How nested menus are generally implemented

The traditional approach to nested menus typically involves:

  1. Hover-based triggers: Parent items open submenus on mouse enter

  2. Timer-based closing: Submenus close after a delay when the mouse leaves

1 2 3 4 5 // Traditional approach const handleMouseEnter = () => setIsOpen(true); const handleMouseLeave = () => { setTimeout(() => setIsOpen(false), 200); };

While this works for simple cases, it creates a significant usability issue when users need to navigate diagonally between menu levels.

The core problem: Diagonal mouse movement

The fundamental issue with traditional nested menu implementations is that they don't account for natural mouse movement patterns. When a user wants to navigate from a parent menu item to a submenu, they rarely move their mouse in a perfectly straight line. Instead, they often move diagonally, which can inadvertently trigger the menu's close behavior.

No More Menu Rage slider 03

Why This Happens

  1. Geometric reality: The shortest path between two points is a straight line, but users naturally move their mouse in diagonal motions

  2. Timing conflicts: The delay between leaving the parent and entering the submenu can cause premature closure

  3. Precision requirements: Users must navigate with pixel-perfect accuracy to avoid triggering close events

This creates a frustrating user experience where menus close unexpectedly, forcing users to restart their navigation multiple times.

Enter the safe area concept

The "safe area" (also known as a "safe triangle") is a UX pattern that creates an invisible zone where the user's mouse can travel without triggering the menu close behavior. This zone is calculated based on the user's mouse movement direction and intent.

The key insight is: If the user is moving toward the submenu, keep it open even if they temporarily leave the parent item's boundaries.

While several open source implementations use SVG overlays and event propagation to calculate the safe zone, we chose a lightweight alternative that relies solely on real-time mouse position tracking, offering greater flexibility and improved performance for our use case.

Key elements for safe area navigation

To implement an effective safe area solution, we need to track several key elements:

1. Target element

The main menu item that reveals the submenu on hover typically a <div>, <li>, or <button>. Tracking mouse entry and exit on this element helps detect when a navigation attempt starts or resets.

2. Content element

The submenu that appears next to the target. It may include nested items. Crucially, it shouldn’t close immediately when the mouse leaves the target if the user is moving toward it.

3. Mouse position tracking

We track the current and previous mouse coordinates to infer movement direction. If the user is heading toward the submenu, we delay closing to allow smooth navigation.

4. Spatial relationships

By calculating the bounding boxes of the target and content, we understand their position and distance. This helps determine whether the user’s mouse is moving in a valid path toward the submenu.

No More Menu Rage spatial relationships 01
No More Menu Rage spatial relationships 02

1/2

Building the hook

A React hook is a reusable function that lets you "hook into" React features like state, context, refs, and lifecycle behavior inside functional components. Hooks help keep logic modular and easy to share across components.

Determining content direction

The first step is to detect the position of the submenu relative to the target element. This helps determine the direction the user is likely to move in, allowing us to better evaluate their intent as they begin navigating toward the content. 

To do this, we calculate the center points of both the target and content elements and compare their positions to determine the relative direction.

1 2 3 4 5 6 7 8 9 export function getContentDirection( targetRect: Pick<DOMRect, 'left' | 'right'>, contentRect: Pick<DOMRect, 'left' | 'right'>, ): Direction { const targetCenter = (targetRect.left + targetRect.right) / 2; const contentCenter = (contentRect.left + contentRect.right) / 2; return contentCenter >= targetCenter ? DIRECTION.RIGHT : DIRECTION.LEFT; }

Left side submenu 

No More Menu Rage - left side submenu

Right side submenu 

No More Menu Rage - right side submenu

Mouse event listeners

To determine if the user is moving toward the submenu, we need to track their mouse position globally, not just over specific elements. This is especially important during diagonal movement, where the cursor may briefly leave both the target and content areas.

We achieve this by listening to mousemove events on the window, allowing continuous tracking of cursor coordinates. This ensures that even transient movement patterns are captured, improving our ability to detect user intent.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const useMousePosition = (callback?: (pos: MousePosition) => void ) => { const mousePositionRef = useRef<MousePosition>({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (ev: MouseEvent) => { mousePositionRef.current.x = ev.clientX; mousePositionRef.current.y = ev.clientY; if (callback) { callback(mousePositionRef.current); } }; window.addEventListener('mousemove', handleMouseMove); return () => { window.removeEventListener('mousemove', handleMouseMove); }; }, [callback]); return mousePositionRef.current; };

Movement prediction

Now that we have both the submenu's direction and real-time mouse tracking in place, we can start evaluating whether the user is intentionally moving toward the submenu.

To do this, we check two key conditions:

  1. Vertical alignment: Is the cursor within the vertical bounds of the submenu?

  2. Horizontal direction: Is the mouse moving toward the submenu based on its position (left or right of the target)?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const getIsMovingTowardsContent = ( mousePos: { x: number; y: number }, prevMousePos: { x: number; y: number }, contentRect: DOMRect, direction: Direction, ) => { const isWithinContentVerticalBounds = mousePos.y >= contentRect.top && mousePos.y <= contentRect.bottom; let isMovingHorizontallyTowardContent = false; if (direction === DIRECTION.RIGHT) { isMovingHorizontallyTowardContent = mousePos.x >= prevMousePos.x && mousePos.x <= contentRect.left; } else { isMovingHorizontallyTowardContent = mousePos.x <= prevMousePos.x && mousePos.x >= contentRect.right; } return isWithinContentVerticalBounds && isMovingHorizontallyTowardContent; };
No more menu rage - Movement prediction

useSafeArea

With all the building blocks in place, we can now combine them into the useSafeArea hook.

At its core, the hook maintains two refs: one for the target element and one for the content element. While also managing the internal state to track when the cursor is in a safe zone.

This hook acts as the central logic hub, orchestrating all the conditions we've defined to ensure smooth and intentional navigation between nested menu elements.

1 2 3 4 5 6 7 8 export const useSafeArea = < TargetElement extends HTMLElement = HTMLElement, ContentElement extends HTMLElement = HTMLElement, >(): { safeZone: boolean; targetRef: React.RefObject<TargetElement>; contentRef: React.RefObject<ContentElement>; } => { /* safe zone computation */ };

Usage of hook in your component

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React from 'react'; import { useSafeArea } from './useSafeArea'; // your custom hook export function NestedMenu() { const { safeZone, targetRef, contentRef } = useSafeArea(); return ( <div className="menu-wrapper"> <div ref={targetRef} className="menu-trigger"> Hover me </div> {safeZone && ( <div ref={contentRef} className="submenu-content"> Submenu content </div> )} </div> ); }

Detecting the safe area

There are three key zones where the user is considered to be in the “safe area”:

  1. The target element — where the interaction begins.

  2. The content element — the visible submenu the user is navigating toward.

  3. The diagonal movement zone — an invisible triangle between the two, where the mouse is moving toward the submenu.

To check if the user is within the target or content element, we use a geometric helper that compares the mouse position against the element's bounding box.

1 2 3 4 5 function isPointInsideRect(point: { x: number; y: number }, rect: DOMRect) { return ( point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom ); }

For diagonal movement, the getIsMovingTowardsContent function helps determine whether the user is actively moving toward the submenu. By combining these checks, we can reliably detect when the user is in the safe zone and prevent the submenu from closing prematurely during natural navigation.

If the user is in any one of the three safe areas, we mark the safe zone as active. Once the user enters the content area, we stop checking for directional movement, since the goal has been achieved. This prevents unnecessary computation and avoids false negatives.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 export const useSafeArea = < TargetElement extends HTMLElement = HTMLElement, ContentElement extends HTMLElement = HTMLElement, >(): { safeZone: boolean; targetRef: React.RefObject<TargetElement>; contentRef: React.RefObject<ContentElement>; } => { const targetRef = useRef<TargetElement | null>(null); const contentRef = useRef<ContentElement | null>(null); const [safeZone, setSafeZone] = useState(false); const [reachedContent, setReachedContent] = useState(false); const prevMousePos = useRef({ x: 0, y: 0 }); /** * Main mouse move handler that evaluates whether the cursor * is within a safe area or moving toward the submenu */ const handleMouseMove = useCallback( ({ x: mouseX, y: mouseY }: { x: number; y: number }) => { const targetRect = targetRef.current?.getBoundingClientRect(); const contentRect = contentRef.current?.getBoundingClientRect(); // Determine whether the content is to the left or right of the target const direction = targetRect && contentRect ? getContentDirection(targetRect, contentRect) : DIRECTION.RIGHT; let inTarget = false; let inContent = false; let isMovingTowardsContent = false; // Check if the mouse is inside the target element if (targetRect) { inTarget = isPointInsideRect({ x: mouseX, y: mouseY }, targetRect); } // Check if the mouse is inside the content element if (contentRect) { inContent = isPointInsideRect({ x: mouseX, y: mouseY }, contentRect); } // If mouse has not reached the submenu yet, check if it's moving toward it if (targetRect && contentRect && !reachedContent) { isMovingTowardsContent = getIsMovingTowardsContent( { x: mouseX, y: mouseY }, prevMousePos.current, contentRect, direction, ); } // Determine if we're in a safe area if (inTarget || inContent || isMovingTowardsContent) { setSafeZone(true); // If the mouse has reached the submenu, we update the state if (inContent) { setReachedContent(true); } // If the mouse returns to the target, reset the reachedContent state to restart the movement calculations if user moves again if (inTarget) { setReachedContent(false); } } else { // If not in any of the safe zones, deactivate the safe state setSafeZone(false); } // Update previous mouse position for next movement comparison prevMousePos.current = { x: mouseX, y: mouseY }; }, [reachedContent], ); // Register the mouse movement listener useMousePosition(handleMouseMove); // Return the refs and the current safeZone state return { safeZone, targetRef, contentRef }; };

Key features

  1. Granular Control with Refs: The hook exposes targetRef and contentRef, giving you direct access to the DOM elements involved in the interaction. This enables accurate hit-testing and lays the foundation for supporting more complex nested menu structures in the future.

  2. Optimized for Performance: Transient values like mouse position and timeouts are stored in useRef, preventing extra renders and keeping the hook lightweight. This means your components remain snappy even during rapid user interaction.

  3. One-Way State Transitions: State transitions — like setting reachedContent when the user enters the submenu — are one-way and deterministic. This avoids flickering or edge-case bugs where the submenu might reopen or close unexpectedly.

  4. Future-Proof Architecture: With clear separation of concerns and a modular design, the hook is built to scale. Whether you want to support vertical submenus, introduce time-based thresholds, or handle mobile gestures, extending the logic will feel intuitive and maintainable.

Conclusion

Building a reliable and frustration-free nested menu experience is trickier than it looks — but it’s also deeply satisfying when you get it right. With useSafeArea, I set out to solve a deceptively simple problem with a combination of geometry, prediction, and React best practices. The result is a lightweight, performant, and extensible hook that lets users navigate menus the way they naturally do without fear of premature closure. If you’ve ever raged at a disappearing submenu, this one’s for you.

Disclaimer

Rippling and its affiliates do not provide tax, accounting, or legal advice. This material has been prepared for informational purposes only, and is not intended to provide or be relied on for tax, accounting, or legal advice. You should consult your own tax, accounting, and legal advisors before engaging in any related activities or transactions.

Author

Headshot of Jasmeet.

Jasmeet Singh

Software Engineer II

Jasmeet Singh is part of Rippling’s Web Design Systems team, building the foundational components that power intuitive and modern user experiences across the platform. When he’s not engineering scalable UI systems, he’s either smashing birdies on the badminton court or out trekking in the mountains, always chasing the next adventure.

Explore more

seo_image_31dd4481_aBAMAKUq0
Aug 21, 2025
|
14 MIN

How Rippling builds enterprise-grade APIs

Our enterprise-grade APIs empower customers to integrate with Rippling seamlessly from their own infrastructure, enabling faster implementation with minimal engineering effort.

seo_image_bc6b2f90_aBAMAKUq0
Aug 21, 2025
|
7 MIN

Building Editors in React Applications

Learn how to build efficient and user-friendly editors in React applications with insights from Rippling's development team.

seo_image_784bf58d_aBAMAKUq0
Aug 21, 2025
|
8 MIN

Inside Rippling’s real-time reporting engine

Rippling's BI tool is better than standalone BI systems. Rippling allows users to merge data across applications into real-time reports to answer the most pressing business questions.

seo_image_b1ed480e_aBAMAKUq0
Aug 21, 2025
|
10 MIN

How to hire a WordPress developer in India

Learn how to hire a WordPress developer in India, including where to source talent, how to vet developers, and more.

Abstract illustration of a cube
Oct 24, 2025
|
9 MIN

How Rippling learned to work differently

Discover how Rippling’s Head of AI created a “treat it as your intern” policy that increased adoption for organization-level wins.

seo_image_b1ed480e_aBAMAKUq0
Aug 21, 2025
|
16 MIN

How to hire a developer in India

Please use top keywords that people could search to get to this blog in the meta description.

seo_image_27124aaa_aBAMAKUq0
Aug 21, 2025
|
2 MIN

Salary under $75K? Our data shows you’re more likely to get laid off

Discover why employees earning under $75,000 were more likely to face layoffs during COVID-19 and what it means for businesses.

[Blog] | Featured image | From Copy‑Paste Chaos to Cohesive Mobile CI/CD | .jpg
Sep 30, 2025
|
7 MIN

From copy‑paste chaos to cohesive mobile CI/CD: Rippling’s journey with Bitrise Modular YAML

Learn how Kushal Agrawal helped Rippling adopt Bitrise Modular YAML for cohesive mobile CI/CD.

See Rippling in action

Increase savings, automate busy work, and make better decisions by managing HR, IT, and Finance in one place.