zeebra
v0.1.1
Published
A performant z-index management library with virtual z-stack recycling
Readme
Zeebra 🦓
A 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.
Features
- 🚀 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-zeebraattribute - 📦 TypeScript - Full TypeScript support
- 🧪 Well tested - Comprehensive unit and integration tests
Installation
npm install zeebraQuick Start
Vanilla 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);React
import { Zeebra } from 'zeebra/react';
function MyDialog() {
return (
<Zeebra>
<div>My draggable dialog</div>
</Zeebra>
);
}Table of Contents
- Core Concepts
- API Reference
- React API
- Configuration
- Advanced Usage
- Performance
- Testing
- Examples
- Contributing
Core Concepts
Virtual Z-Stack
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 + virtualPositionZ-Index Recycling
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]
CSS Variables
Zeebra uses CSS custom properties (--zeebra-z-index) to update z-indexes. This approach:
- Avoids triggering React re-renders
- Allows CSS to handle the actual z-index application
- Enables batched updates via
requestAnimationFrame
API Reference
Core Methods
register(element: HTMLElement): void
Register an element in the z-stack. The element will be assigned the next available z-index.
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.
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): Iftrue, updates CSS immediately instead of batching. Default:false
// 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.
zeebra.lower(dialog);
zeebra.lower(dialog, true); // Immediate updatebringToFront(element: HTMLElement, sync?: boolean): void
Alias for lift(). Brings element to the front.
zeebra.bringToFront(dialog);sendToBack(element: HTMLElement, sync?: boolean): void
Alias for lower(). Sends element to the back.
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.
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.
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).
const elements = zeebra.getTrackedElements();
console.log(elements.length); // Number of tracked elementsgetNextZIndex(): number
Get the next available z-index value (useful for manual z-index assignment).
const nextZIndex = zeebra.getNextZIndex();
console.log(nextZIndex); // 5000 + number of tracked elementsisRegistered(element: HTMLElement): boolean
Check if an element is registered in the z-stack.
if (zeebra.isRegistered(dialog)) {
// Element is tracked
}React API
<Zeebra> Component
Wrapper component that automatically manages z-index for its children.
import { Zeebra } from 'zeebra/react';
function MyDialog() {
return (
<Zeebra>
<div>My dialog content</div>
</Zeebra>
);
}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
import { Zeebra, ZeebraRef } from 'zeebra/react';
function MyDialog() {
const zeebraRef = useRef<ZeebraRef>(null);
const handleClick = () => {
zeebraRef.current?.lift();
};
return (
<Zeebra ref={zeebraRef}>
<button onClick={handleClick}>Bring to Front</button>
</Zeebra>
);
}Ref Methods:
lift()- Lift element to toplower()- Lower element to bottombringToFront()- Alias for liftsendToBack()- Alias for lowergetElement()- Get the underlying DOM element
useZeebra() Hook
Hook for programmatic z-index control with your own refs.
import { useZeebra } from 'zeebra/react';
function MyDialog() {
const elementRef = useRef<HTMLDivElement>(null);
const { lift, lower, bringToFront, sendToBack } = useZeebra(elementRef);
return (
<div ref={elementRef}>
<button onClick={lift}>Bring to Front</button>
<button onClick={lower}>Send to Back</button>
</div>
);
}Returns:
lift: () => void- Lift element to toplower: () => void- Lower element to bottombringToFront: () => void- Alias for liftsendToBack: () => void- Alias for lowerregister: () => void- Manually register elementunregister: () => void- Manually unregister element
Configuration
CSS Variable: --zeebra-start
Set the starting z-index value using a CSS variable:
:root {
--zeebra-start: 5000; /* Default is 5000 */
}Elements will use z-indexes starting from this value and incrementing based on their virtual position.
Auto-Detection
Elements with the data-zeebra attribute are automatically registered:
<div data-zeebra>
This element is automatically tracked
</div>Auto-detection runs on:
- DOM ready
- Dynamically added elements (via MutationObserver)
Advanced Usage
Synchronous Updates
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:
// Sync mode - updates immediately
zeebra.lift(element, true);
const zIndex = zeebra.getZIndex(element); // Available immediatelyManual Z-Index Application
If you need to apply z-index to a parent container (like with react-rnd):
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);
}Multiple Z-Stack Instances
The library uses a global instance by default. For multiple independent z-stacks, you can create separate instances:
import { VirtualZStack } from 'zeebra/virtual-zstack';
const customStack = new VirtualZStack();
customStack.register(element);Event-Driven Updates
Listen to z-index changes by polling or using a custom event system:
// 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
Batched Updates
Zeebra batches CSS updates using requestAnimationFrame, ensuring:
- All updates happen in a single frame
- Minimal DOM manipulation
- No layout thrashing
CSS Variables
Using CSS variables instead of direct style updates:
- Avoids React re-renders
- Leverages browser's CSS engine
- Enables CSS-based styling
WeakMap Storage
Element references are stored in WeakMaps, allowing:
- Automatic garbage collection
- No memory leaks
- Efficient lookups
Testing
The library includes comprehensive unit and integration tests.
Run Tests
# Run all tests
npm test
# Watch mode
npm run test:watch
# With coverage
npm run test:coverage
# CI mode (JUnit XML output)
npm run test:ciTest Structure
- Unit Tests: Core logic, utilities, CSS manager
- Integration Tests: Full workflows, DOM interactions, React components
See tests/README.md for details.
Examples
Draggable Windows
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...
}
});React Draggable Dialog
import { Zeebra } from 'zeebra/react';
import { Rnd } from 'react-rnd';
function DraggableDialog() {
const zeebraRef = useRef(null);
const windowRef = useRef(null);
return (
<Zeebra ref={zeebraRef}>
<Rnd
onDragStart={() => {
zeebraRef.current?.lift();
}}
>
<div ref={windowRef} className="dialog">
Dialog content
</div>
</Rnd>
</Zeebra>
);
}Modal Stack Management
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:
import zeebra, { HTMLElement } from 'zeebra';
const element: HTMLElement = document.getElementById('dialog')!;
zeebra.register(element);Contributing
Contributions are welcome! Please see our contributing guidelines.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
License
MIT
Changelog
See CHANGELOG.md for version history.
