Skip to main content

Z-Index Choreography for a Mobile Bottom Nav

A layered architectural structure showing different levels
Apr 6, 20262 min readCSS, Mobile, Accessibility, React

The Layering Problem

Adding a fixed bottom navigation bar to a mobile app sounds simple, but this site already had two fixed-position elements down at the bottom of the screen: a scroll-to-top FAB on the right and a table-of-contents FAB on the left, both sitting at bottom: 1.5rem and z-index: 50.

Drop a h-14 fixed bottom bar at z-index: 50 and both buttons vanish behind it, so you tap where the scroll-to-top button should be and hit the nav bar instead.

The Z-Index Stack

The fix needed a deliberate stacking order, not just "make it higher." I settled on three layers:

Layerz-indexElements
FABs40ScrollToTop, ToC trigger
Navigation50Bottom bar, backdrop
Sheets51ToC bottom sheet, More menu sheet

FABs sit below the nav bar because they're secondary actions: you can always scroll to find the content, but you always need the nav. Sheets that slide up from the bottom have to cover the nav bar, so they get z-51.

// ScrollToTop — z-40, raised above nav on mobile
'fixed bottom-20 md:bottom-6 right-6 z-40 ...';
 
// Bottom nav bar — z-50
'fixed bottom-0 left-0 right-0 z-50 bg-surface/95 ...';
 
// ToC bottom sheet — z-51, covers nav when open
'fixed bottom-0 left-0 right-0 z-51 bg-surface ...';

The Responsive Escape Hatch

The bottom nav only exists on mobile (md:hidden); desktop keeps a traditional sticky top bar. So the FABs need different positioning per breakpoint:

className = 'fixed bottom-20 md:bottom-6 right-6 z-40 ...';

bottom-20 (5rem) clears the h-14 (3.5rem) nav bar with some breathing room on mobile, and md:bottom-6 restores the original position on desktop where there's no bottom bar at all. One utility class, no JavaScript.

Reusing the Sheet Pattern

The site already had a bottom sheet for the table of contents: a translate-y transition that slides up from off-screen:

<div
  className={cn(
    'fixed bottom-0 left-0 right-0 z-51 ...',
    isOpen ? 'translate-y-0' : 'translate-y-full pointer-events-none'
  )}
>

The "More" menu in the new bottom nav reuses the exact same pattern: translate-y-0 when open, translate-y-full pointer-events-none when closed, with the same focus trap hook, the same Escape key handler, and the same body scroll lock. When the animation patterns stay consistent, users build the mental model once and it carries over everywhere.

Accessibility Considerations

A fixed bottom bar introduces several a11y requirements:

  • aria-current="page" on the active nav link, not just a color change
  • aria-expanded on the More button to communicate sheet state
  • aria-modal="true" on the sheet dialog
  • Focus trap inside the sheet when open (via useFocusTrap hook)
  • Escape key closes the sheet and returns focus to the More button
  • Body scroll lock prevents background scrolling while the sheet is open

The More button's label also changes dynamically:

aria-label={sheetOpen ? 'Close more menu' : 'Open more menu'}

The Takeaway

Fixed-position elements are easy to add one at a time; the complexity is all in how they interact. Defining a z-index stack as a deliberate system rather than ad-hoc increments heads off the "just make it higher" escalation that eventually lands you at z-index: 9999. Three layers with clear ownership is all this site needs, and it stays that way because nobody has to guess.