2024Libraries
zeebra
Back

zeebra

Performant z-index management library with virtual z-stack recycling for complex UI layering.

z-index: 9999

We've all done it. Don't lie.

You need a modal to appear above a dropdown. The dropdown is at z-index 10. So you make the modal z-index 100. Then someone adds a tooltip that needs to appear above both. z-index 1000. Then a notification system. z-index 10000. Then another modal that appears OVER the first modal.

Before you know it, you have z-index values that look like phone numbers.

The Problem Is Worse Than You Think

Z-index issues are notoriously hard to debug. Elements disappear behind other elements and nobody knows why. Stack contexts are created in ways that seem random. And everyone's solution is "just make the number bigger."

I was debugging a modal that wouldn't appear above a dropdown. The modal had z-index: 9999. The dropdown had z-index: 10. By all logic, the modal should win.

But the dropdown was inside a transformed element. Transforms create new stacking contexts. The modal wasn't even in the same stacking context as the dropdown. The numbers were meaningless.

That's when I decided: I'm never debugging z-index again.

How Zeebra Works

Instead of hardcoding z-index values, you ask Zeebra for one:

tsx
const zIndex = useZIndex('modal');

Zeebra knows that modals should be above dropdowns, which should be above tooltips, which should be above base content. It returns the right value automatically.

When your component unmounts, Zeebra recycles the z-index. This is the "virtual z-stack" part — we're not accumulating infinite...

Built With

TypeScriptReactNPM

Impact

Focus

Performance

Platform

npm

Under the Hood

A peek at the implementation — the kind of code that powers zeebra.

zeebra.tsxtypescript
1// No more z-index: 99999
2// Just ask for a layer and trust the system
3
4import { createContext, useContext, useState, useCallback, useEffect } from 'react';
5
6type LayerPriority = 'base' | 'dropdown' | 'tooltip' | 'modal' | 'notification';
7
8interface ZIndexState {
9  layers: Map<string, number>;
10  allocate: (id: string, priority: LayerPriority) => number;
11  release: (id: string) => void;
12}
13
14const PRIORITY_RANGES: Record<LayerPriority, [number, number]> = {
15  base:         [1, 99],
16  dropdown:     [100, 199],
17  tooltip:      [200, 299],
18  modal:        [300, 399],
19  notification: [400, 499],
20};
21
22function createZIndexManager(): ZIndexState {
23  const layers = new Map<string, number>();
24  const usedInRange = new Map<LayerPriority, Set<number>>();
25
26  // Initialize used sets
27  for (const priority of Object.keys(PRIORITY_RANGES)) {
28    usedInRange.set(priority as LayerPriority, new Set());
29  }
30
31  return {
32    layers,
33    
34    allocate(id: string, priority: LayerPriority): number {
35      // Already allocated? Return existing
36      if (layers.has(id)) return layers.get(id)!;
37
38      const [min, max] = PRIORITY_RANGES[priority];
39      const used = usedInRange.get(priority)!;
40
41      // Find first available in range
42      for (let z = min; z <= max; z++) {
43        if (!used.has(z)) {
44          used.add(z);
45          layers.set(id, z);
46          return z;
47        }
48      }
49
50      // Range exhausted - this shouldn't happen in practice
51      console.warn(`zeebra: ${priority} layer range exhausted`);
52      return max;
53    },
54    
55    release(id: string): void {
56      const z = layers.get(id);
57      if (z === undefined) return;
58
59      // Find which priority this belonged to
60      for (const [priority, [min, max]] of Object.entries(PRIORITY_RANGES)) {
61        if (z >= min && z <= max) {
62          usedInRange.get(priority as LayerPriority)!.delete(z);
63          break;
64        }
65      }
66
67      layers.delete(id);
68    }
69  };
70}
71
72// The hook that makes it all easy
73export function useZIndex(priority: LayerPriority): number {
74  const manager = useContext(ZIndexContext);
75  const [id] = useState(() => `zeebra-${Math.random().toString(36).slice(2)}`);
76  const [zIndex, setZIndex] = useState<number>(0);
77
78  useEffect(() => {
79    const z = manager.allocate(id, priority);
80    setZIndex(z);
81
82    return () => manager.release(id);
83  }, [id, priority, manager]);
84
85  return zIndex;
86}

Discussion

💬 Be nice, or be funny. Preferably both.