14 KiB
Z-Index & Stacking Context Management
Overview
This document tracks z-index values and stacking contexts across the application to prevent layering conflicts and make reasoning about visual hierarchy easy.
The Z-Index Constants System
Location: src/constants/zIndex.ts
All z-index values should be defined in this file and imported where needed:
import { Z_INDEX } from "../constants/zIndex";
// Use it like this:
zIndex: Z_INDEX.NAV_BAR;
zIndex: Z_INDEX.MODAL;
zIndex: Z_INDEX.GAME_NAV.HAMBURGER_MENU;
Z-Index Layering Hierarchy
From lowest to highest:
| Layer | Range | Purpose | Examples |
|---|---|---|---|
| Base Content | 0-99 | Default page content, game elements | Background elements, game tracks, cards |
| Navigation & UI Chrome | 100-999 | Fixed navigation, sticky headers | AppNavBar, page headers |
| Overlays & Dropdowns | 1000-9999 | Tooltips, popovers, dropdowns, tutorial tooltips | Tutorial tooltips (50-100), ConfigForm (50), dropdowns (999-1000) |
| Modals & Dialogs | 10000-19999 | Modal dialogs, confirmation dialogs | Modal backdrop (10000), Modal content (10001) |
| Top-Level Overlays | 20000+ | Toasts, critical notifications | Toast notifications (20000) |
Stacking Context Rules
What Creates a Stacking Context?
These CSS properties create new stacking contexts (z-index values are relative within them):
position: fixedorposition: stickywith z-indexposition: absoluteorposition: relativewith z-indexopacity< 1transform(any value)filter(any value except none)isolation: isolate
Key Insight
Z-index values are only compared within the same stacking context!
If Element A creates a stacking context with z-index: 1 and Element B is outside that context with z-index: 999, Element B will be on top regardless of child z-indexes inside Element A.
Example
// Parent creates stacking context
<div style={{ position: 'relative', zIndex: 1 }}>
{/* This child's z-index is relative to parent, not global! */}
<div style={{ position: 'absolute', zIndex: 999999 }}>
I'm still under elements with zIndex: 2 outside my parent!
</div>
</div>
<div style={{ position: 'relative', zIndex: 2 }}>
I'm on top of the z-index: 999999 element above!
</div>
Current Z-Index Audit (2025-10-20)
✅ Using Z_INDEX Constants (Good!)
| Component | Value | Source |
|---|---|---|
| AppNavBar (Panda section) | Z_INDEX.NAV_BAR (100) |
src/components/AppNavBar.tsx:464 |
| AppNavBar hamburger | Z_INDEX.GAME_NAV.HAMBURGER_MENU (9999) |
src/components/AppNavBar.tsx:165 |
| AbacusDisplayDropdown | Z_INDEX.GAME_NAV.HAMBURGER_NESTED_DROPDOWN (10000) |
src/components/AbacusDisplayDropdown.tsx:99 |
⚠️ Hardcoded Z-Index Values (Need Migration)
Critical Navigation Issues
| Component | Line | Value | Issue | Fix |
|---|---|---|---|---|
| AppNavBar (fixed section) | 587 | 1000 |
❌ Should use Z_INDEX.NAV_BAR (100), but increased to 1000 to fix tutorial tooltip overlap |
Define TUTORIAL_TOOLTIP in constants, set nav to proper layer |
| AppNavBar (badge) | 645 | 50 |
Should use constant | Add Z_INDEX.BADGE |
Tutorial System
| Component | Line | Value | Purpose |
|---|---|---|---|
| TutorialPlayer | 643 | 50 |
Tooltip container |
| Tutorial shared/EditorComponents | 569, 590 | 50 |
Tooltip button |
| Tutorial shared/EditorComponents | 612 | 100 |
Dropdown content (must be above tooltip) |
| Tutorial decomposition CSS | 73 | 50 |
Legacy CSS |
| TutorialEditor | 65, 812, 2339 | 1000, 10 |
Various overlays |
Modals & Overlays
| Component | Line | Value | Purpose |
|---|---|---|---|
| Modal (common) | 59 | 10000 |
Modal backdrop |
| ModerationPanel | 1994, 2009 | 10001, 10002 |
Moderation overlays |
| ToastContext | 171 | 10001 |
Toast notifications (should be 20000!) |
| Join page | 35 | 10000 |
Join page overlay |
| EmojiPicker | 636 | 10000 |
Emoji picker modal |
Dropdowns & Popovers
| Component | Line | Value | Purpose |
|---|---|---|---|
| FormatSelectField | 115 | 999 |
Dropdown |
| DeploymentInfoModal | 37, 55 | 9998, 9999 |
Info modal layers |
| RoomInfo | 338, 562 | 9999, 10000 |
Room tooltips |
| GameTitleMenu | 119 | 9999 |
Game menu |
| PlayerTooltip | 69 | 9999 |
Player tooltip |
Game Elements
| Component | Line | Value | Purpose |
|---|---|---|---|
| Complement Race Game | Multiple | 0, 1 |
Base game layers |
| Complement Race Track | 118, 140, 151 | 10, 5, 20 |
Track, AI racers, player |
| Complement Race HUD | 51, 106, 119, 137, 168 | 10, 1000 |
HUD elements |
| GameCountdown | 58 | 1000 |
Countdown overlay |
| RouteCelebration | 31 | 9999 |
Celebration overlay |
| Matching GameCard | 203, 229, 243, 272, 386 | 9, 10, -1, 8, 1 |
Card layers |
| Matching PlayerStatusBar | 154, 181, 202 | 10, 10, 5 |
Status bars |
Misc UI
| Component | Line | Value | Purpose |
|---|---|---|---|
| HeroAbacus | 89, 127, 163 | 10 |
Hero section layers |
| ChampionArena | 425, 514, 554, 614 | 10, 1, 1, 10 |
Arena layers |
| NetworkPlayerIndicator | 118, 145, 169, 192, 275 | -1, 2, 1, 2, 10 |
Player avatars |
| ConfigurationForm | 521, 502 | 50 |
Config overlays |
The Recent Bug: Tutorial Tooltips Over Nav Bar
Problem: Tutorial tooltips (z-index: 50, 100) were appearing over the navigation bar.
Root Cause:
- Nav bar was using
Z_INDEX.NAV_BAR= 100 in one place - But also hardcoded
zIndex: 30in the fixed positioning section (line 587) - Tutorial tooltips use hardcoded
zIndex: 50andzIndex: 100 - Since 50 and 100 > 30, tooltips appeared on top
Temporary Fix: Increased nav bar's hardcoded value from 30 to 1000
Proper Fix Needed:
- Define tutorial tooltip z-indexes in constants file
- Update nav bar to consistently use
Z_INDEX.NAV_BAR - Ensure NAV_BAR > TUTORIAL_TOOLTIP in the hierarchy
- Consider: Should tutorial tooltips be in the 1000-9999 range (overlays) rather than 50-100?
Guidelines for Choosing Z-Index Values
1. Always Import and Use Z_INDEX Constants
// ✅ Good
import { Z_INDEX } from "../constants/zIndex";
zIndex: Z_INDEX.NAV_BAR;
// ❌ Bad
zIndex: 100; // Magic number!
2. Add New Values to Constants File First
Before using a new z-index value, add it to src/constants/zIndex.ts:
export const Z_INDEX = {
// ... existing values ...
TUTORIAL: {
TOOLTIP: 500, // Tutorial tooltips (overlays layer)
DROPDOWN: 600, // Tutorial dropdown (above tooltip)
},
} as const;
3. Choose the Right Layer
Ask yourself:
- Is this base content? → Use 0-99
- Is this navigation/UI chrome? → Use 100-999
- Is this a dropdown/tooltip/overlay? → Use 1000-9999
- Is this a modal dialog? → Use 10000-19999
- Is this a toast notification? → Use 20000+
4. Understand Your Stacking Context
Before setting z-index, ask:
- What is my parent's stacking context?
- Am I comparing against siblings or global elements?
- Does my element create a new stacking context?
5. Document Special Cases
If you must deviate from the constants, document why:
// HACK: Needs to be above tutorial tooltips (50) but below modals (10000)
// TODO: Migrate to Z_INDEX.TUTORIAL.TOOLTIP system
zIndex: 100;
Migration Plan
Phase 1: Update Constants File ✅ TODO
Add missing constants to src/constants/zIndex.ts:
export const Z_INDEX = {
// Base content layer (0-99)
BASE: 0,
CONTENT: 1,
HERO_SECTION: 10, // Hero abacus components
// Game content layers (0-99)
GAME_CONTENT: {
TRACK: 0,
CONTROLS: 1,
RACER_AI: 5,
RACER_PLAYER: 10,
RACER_FLAG: 20,
HUD: 50,
},
// Navigation and UI chrome (100-999)
NAV_BAR: 1000, // ⚠️ Currently needs to be 1000 due to tutorial tooltips
STICKY_HEADER: 100,
BADGE: 50,
// Overlays and dropdowns (1000-9999)
TUTORIAL: {
TOOLTIP: 500, // Tutorial tooltips
DROPDOWN: 600, // Tutorial dropdowns (must be > tooltip)
EDITOR: 700, // Tutorial editor
},
DROPDOWN: 1000,
TOOLTIP: 1000,
POPOVER: 1000,
CONFIG_FORM: 1000,
PLAYER_TOOLTIP: 1000,
GAME_COUNTDOWN: 1000,
// High overlays (9000-9999)
CELEBRATION: 9000,
INFO_MODAL: 9998,
// Modal and dialog layers (10000-19999)
MODAL_BACKDROP: 10000,
MODAL: 10001,
MODERATION_PANEL: 10001,
EMOJI_PICKER: 10000,
// Top-level overlays (20000+)
TOAST: 20000,
// Special navigation layers for game pages
GAME_NAV: {
HAMBURGER_MENU: 9999,
HAMBURGER_NESTED_DROPDOWN: 10000,
},
} as const;
Phase 2: Migrate High-Priority Components
Priority order:
- Navigation components (AppNavBar, etc.) - most critical for user experience
- Tutorial system (TutorialPlayer, tooltips) - currently conflicting
- Modals and overlays - ensure they're always on top
- Game HUDs - ensure proper layering
- Everything else
Phase 3: Add Linting Rule
Consider adding an ESLint rule to prevent raw z-index numbers:
// Warn when zIndex is used with a number literal
'no-magic-numbers': ['warn', {
ignore: [0, 1, -1],
ignoreArrayIndexes: true,
enforceConst: true,
}]
Debugging Z-Index Issues
Checklist
When elements aren't layering correctly:
-
Check the value
- What z-index does each element have?
- Are they using constants or magic numbers?
-
Check the stacking context
- What are the parent elements?
- Do any parents create stacking contexts? (position + z-index, opacity, transform, etc.)
- Are we comparing siblings or elements in different contexts?
-
Verify the DOM hierarchy
- Use browser DevTools to inspect the DOM tree
- Check the "Layers" panel in Chrome DevTools
- Look for transforms, opacity, filters on parent elements
-
Test the fix
- Does the fix work in all scenarios?
- Did we introduce new conflicts?
- Should we update the constants file?
DevTools Tips
Chrome DevTools:
- Open DevTools → More Tools → Layers
- Select an element and see its stacking context
- View the 3D layer composition
Firefox DevTools:
- Inspector → Layout → scroll to "Z-index"
- Shows the stacking context parent
Examples
Good: Using Constants
import { Z_INDEX } from '@/constants/zIndex'
export function MyTooltip() {
return (
<div className={css({
position: 'absolute',
zIndex: Z_INDEX.TOOLTIP, // ✅ Clear and maintainable
})}>
Tooltip content
</div>
)
}
Bad: Magic Numbers
export function MyTooltip() {
return (
<div className={css({
position: 'absolute',
zIndex: 500, // ❌ Where did 500 come from? How does it relate to other elements?
})}>
Tooltip content
</div>
)
}
Good: Documenting Stacking Context
// Creates a new stacking context for card contents
<div className={css({
position: 'relative',
zIndex: Z_INDEX.BASE,
transform: 'translateZ(0)', // ⚠️ Creates stacking context!
})}>
{/* Child z-indexes are relative to this context */}
<div className={css({
position: 'absolute',
zIndex: Z_INDEX.CONTENT, // Relative to parent, not global
})}>
Card face
</div>
</div>
Resources
- MDN: CSS Stacking Context
- What The Heck, z-index?? by Josh Comeau
- Z-Index Playground
Last Updated
2025-10-20 - Initial audit and documentation created