A performant z-index management library with virtual z-stack recycling
npm install zeebraA performant z-index management library with virtual z-stack recycling. Zeebra efficiently manages z-indexes for floating elements (like draggable dialogs) by maintaining a virtual stack and recycling z-index values within a configurable range.
- ๐ Super performant - Uses CSS variables and batched updates to avoid component re-renders
- โป๏ธ Z-index recycling - Automatically recycles z-indexes within your range
- ๐ฏ Zero wrapper required - Works directly with DOM elements
- โ๏ธ React support - Optional wrapper component and hooks for React developers
- ๐ง Auto-detection - Automatically detects elements with data-zeebra attribute
- ๐ฆ TypeScript - Full TypeScript support
- ๐งช Well tested - Comprehensive unit and integration tests
``bash`
npm install zeebra
`javascript
import zeebra from 'zeebra';
const element = document.getElementById('my-dialog');
// Register the element
zeebra.register(element);
// Lift to top
zeebra.lift(element);
// Lower to bottom
zeebra.lower(element);
`
`tsx
import { Zeebra } from 'zeebra/react';
function MyDialog() {
return (
My draggable dialog
);
}
`
- Core Concepts
- API Reference
- React API
- Configuration
- Advanced Usage
- Performance
- Testing
- Examples
- Contributing
Zeebra maintains a virtual z-stack in memory. Each registered element gets a virtual position (0, 1, 2, ...). The actual z-index is calculated as:
``
z-index = --zeebra-start + virtualPosition
When you lift an element, it moves to the end of the virtual stack (highest z-index). Other elements maintain their relative order but get new z-index values. This recycling ensures you never leave your configured range.
Example:
- Elements: [A: 5000, B: 5001, C: 5002]
- lift(A) โ [B: 5000, C: 5001, A: 5002]
Zeebra uses CSS custom properties (--zeebra-z-index) to update z-indexes. This approach:requestAnimationFrame
- Avoids triggering React re-renders
- Allows CSS to handle the actual z-index application
- Enables batched updates via
#### register(element: HTMLElement): void
Register an element in the z-stack. The element will be assigned the next available z-index.
`javascript`
const dialog = document.getElementById('dialog');
zeebra.register(dialog);
#### unregister(element: HTMLElement): boolean
Remove an element from the z-stack. Returns true if the element was registered, false otherwise.
`javascript`
zeebra.unregister(dialog);
#### lift(element: HTMLElement, sync?: boolean): void
Raise an element to the top (highest z-index). Auto-registers if not already registered.
- sync (optional): If true, updates CSS immediately instead of batching. Default: false
`javascript
// Async update (batched)
zeebra.lift(dialog);
// Sync update (immediate)
zeebra.lift(dialog, true);
`
#### lower(element: HTMLElement, sync?: boolean): void
Lower an element to the bottom (lowest z-index). Auto-registers if not already registered.
`javascript`
zeebra.lower(dialog);
zeebra.lower(dialog, true); // Immediate update
#### bringToFront(element: HTMLElement, sync?: boolean): void
Alias for lift(). Brings element to the front.
`javascript`
zeebra.bringToFront(dialog);
#### sendToBack(element: HTMLElement, sync?: boolean): void
Alias for lower(). Sends element to the back.
`javascript`
zeebra.sendToBack(dialog);
#### getZIndex(element: HTMLElement): number | null
Get the current z-index value for an element (synchronously calculated). Returns null if the element is not registered.
`javascript`
const zIndex = zeebra.getZIndex(dialog);
console.log(zIndex); // 5000, 5001, etc.
#### getVirtualPosition(element: HTMLElement): number | null
Get the virtual position of an element in the z-stack. Returns null if not registered.
`javascript`
const position = zeebra.getVirtualPosition(dialog);
console.log(position); // 0, 1, 2, etc.
#### getTrackedElements(): HTMLElement[]
Get all currently tracked elements in order (lowest to highest z-index).
`javascript`
const elements = zeebra.getTrackedElements();
console.log(elements.length); // Number of tracked elements
#### getNextZIndex(): number
Get the next available z-index value (useful for manual z-index assignment).
`javascript`
const nextZIndex = zeebra.getNextZIndex();
console.log(nextZIndex); // 5000 + number of tracked elements
#### isRegistered(element: HTMLElement): boolean
Check if an element is registered in the z-stack.
`javascript`
if (zeebra.isRegistered(dialog)) {
// Element is tracked
}
Wrapper component that automatically manages z-index for its children.
`tsx
import { Zeebra } from 'zeebra/react';
function MyDialog() {
return (
My dialog content
);
}
`
#### Props
- children: React.ReactNode - Child elements to wrapautoRegister?: boolean
- - Auto-register on mount (default: true)onRegister?: (element: HTMLElement) => void
- - Callback when element is registeredonUnregister?: (element: HTMLElement) => void
- - Callback when element is unregistered
- All standard HTML div props are supported
#### Ref API
`tsx
import { Zeebra, ZeebraRef } from 'zeebra/react';
function MyDialog() {
const zeebraRef = useRef
const handleClick = () => {
zeebraRef.current?.lift();
};
return (
);
}
`
Ref Methods:
- lift() - Lift element to toplower()
- - Lower element to bottombringToFront()
- - Alias for liftsendToBack()
- - Alias for lowergetElement()
- - Get the underlying DOM element
Hook for programmatic z-index control with your own refs.
`tsx
import { useZeebra } from 'zeebra/react';
function MyDialog() {
const elementRef = useRef
const { lift, lower, bringToFront, sendToBack } = useZeebra(elementRef);
return (
Returns:
-
lift: () => void - Lift element to top
- lower: () => void - Lower element to bottom
- bringToFront: () => void - Alias for lift
- sendToBack: () => void - Alias for lower
- register: () => void - Manually register element
- unregister: () => void - Manually unregister elementConfiguration
$3
Set the starting z-index value using a CSS variable:
`css
:root {
--zeebra-start: 5000; / Default is 5000 /
}
`Elements will use z-indexes starting from this value and incrementing based on their virtual position.
$3
Elements with the
data-zeebra attribute are automatically registered:`html
This element is automatically tracked
`Auto-detection runs on:
- DOM ready
- Dynamically added elements (via MutationObserver)
Advanced Usage
$3
By default, z-index updates are batched using
requestAnimationFrame for performance. When you need the z-index immediately (e.g., for a third-party library), use sync mode:`javascript
// Sync mode - updates immediately
zeebra.lift(element, true);
const zIndex = zeebra.getZIndex(element); // Available immediately
`$3
If you need to apply z-index to a parent container (like with react-rnd):
`javascript
zeebra.lift(element, true);
const zIndex = zeebra.getZIndex(element);// Apply to parent container
const container = element.parentElement;
if (container && zIndex !== null) {
container.style.zIndex = String(zIndex);
}
`$3
The library uses a global instance by default. For multiple independent z-stacks, you can create separate instances:
`javascript
import { VirtualZStack } from 'zeebra/virtual-zstack';const customStack = new VirtualZStack();
customStack.register(element);
`$3
Listen to z-index changes by polling or using a custom event system:
`javascript
// Poll for changes
setInterval(() => {
const tracked = zeebra.getTrackedElements();
tracked.forEach(el => {
const zIndex = zeebra.getZIndex(el);
// Update UI or sync with external system
});
}, 100);
`Performance
$3
Zeebra batches CSS updates using
requestAnimationFrame, ensuring:
- All updates happen in a single frame
- Minimal DOM manipulation
- No layout thrashing$3
Using CSS variables instead of direct style updates:
- Avoids React re-renders
- Leverages browser's CSS engine
- Enables CSS-based styling
$3
Element references are stored in WeakMaps, allowing:
- Automatic garbage collection
- No memory leaks
- Efficient lookups
Testing
The library includes comprehensive unit and integration tests.
$3
`bash
Run all tests
npm testWatch mode
npm run test:watchWith coverage
npm run test:coverageCI mode (JUnit XML output)
npm run test:ci
`$3
- Unit Tests: Core logic, utilities, CSS manager
- Integration Tests: Full workflows, DOM interactions, React components
See tests/README.md for details.
Examples
$3
`javascript
import zeebra from 'zeebra';// Create draggable window
const window = document.createElement('div');
window.className = 'window';
document.body.appendChild(window);
// Register with zeebra
zeebra.register(window);
// Lift on click
window.addEventListener('click', () => {
zeebra.lift(window);
});
// Lift on drag start
window.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('window-header')) {
zeebra.lift(window);
// Start drag...
}
});
`$3
`tsx
import { Zeebra } from 'zeebra/react';
import { Rnd } from 'react-rnd';function DraggableDialog() {
const zeebraRef = useRef(null);
const windowRef = useRef(null);
return (
onDragStart={() => {
zeebraRef.current?.lift();
}}
>
Dialog content
);
}
`$3
`javascript
const modals = [];function openModal(content) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = content;
document.body.appendChild(modal);
zeebra.register(modal);
modals.push(modal);
// Automatically bring new modal to front
zeebra.lift(modal);
}
function closeModal(modal) {
zeebra.unregister(modal);
modal.remove();
modals = modals.filter(m => m !== modal);
}
`Browser Support
- Modern browsers with CSS custom properties support
- IE11 not supported (requires CSS variables)
TypeScript
Full TypeScript definitions are included:
`typescript
import zeebra, { HTMLElement } from 'zeebra';const element: HTMLElement = document.getElementById('dialog')!;
zeebra.register(element);
``Contributions are welcome! Please see our contributing guidelines.
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request
MIT
See CHANGELOG.md for version history.