Z-Index
Overview
The z-index composable helps you create a consistent stacking order system with minimal code. It generates z-index variables that can be easily referenced throughout your application, preventing the #1 source of CSS stacking bugs — conflicting arbitrary z-index values.
Why use z-index composables?
Z-index composables help you:
- Prevent stacking conflicts: Use a predefined scale instead of inventing arbitrary values that inevitably collide.
- Establish semantic layers: Name your z-index levels by purpose (dropdown, modal, toast) rather than magic numbers.
- Enable flexible theming: Override z-index variables to adjust stacking order across your application.
- Maintain consistency: Ensure every developer on the team uses the same stacking scale.
- Pair with elevation: Align z-index levels with box-shadow elevation for a coherent visual hierarchy.
useZIndex
Styleframe provides a set of carefully crafted default z-index values that you can use out of the box:
import { styleframe } from 'styleframe';
import { useZIndex } from '@styleframe/theme';
const s = styleframe();
const {
zIndex,
zIndexHide,
zIndexBase,
zIndexDropdown,
zIndexSticky,
zIndexOverlay,
zIndexModal,
zIndexPopover,
zIndexToast,
zIndexMax,
zIndexAuto,
} = useZIndex(s);
export default s;
:root {
--z-index--hide: -1;
--z-index--base: 0;
--z-index--dropdown: 100;
--z-index--sticky: 200;
--z-index--overlay: 300;
--z-index--modal: 400;
--z-index--popover: 500;
--z-index--toast: 600;
--z-index--max: 9999;
--z-index--auto: auto;
--z-index: var(--z-index--base);
}
The default values include:
hide: Behind base content (-1)base(default): Default stacking level (0)dropdown: Dropdowns and select menus (100)sticky: Sticky headers and columns (200)overlay: Overlays and backdrops (300)modal: Modals and dialogs (400)popover: Popovers and tooltips (500)toast: Toasts and notifications (600)max: Escape hatch for highest priority (9999)auto: Browser default (auto)
max token (9999) acts as an escape hatch for edge cases.Extending the Default Z-Index Values
You can customize which z-index is used as the default while keeping all other standard values. Use the @ prefix to reference another key in the values object:
import { styleframe } from 'styleframe';
import { useZIndex, zIndexValues } from '@styleframe/theme';
const s = styleframe();
const { zIndex } = useZIndex(s, {
...zIndexValues,
default: '@dropdown'
});
export default s;
:root {
--z-index--hide: -1;
--z-index--base: 0;
--z-index--dropdown: 100;
--z-index--sticky: 200;
--z-index--overlay: 300;
--z-index--modal: 400;
--z-index--popover: 500;
--z-index--toast: 600;
--z-index--max: 9999;
--z-index--auto: auto;
--z-index: var(--z-index--dropdown);
}
Creating Custom Z-Index Variables
The useZIndex() function creates a set of z-index variables for establishing stacking order in your interface.
import { styleframe } from 'styleframe';
import { useZIndex } from '@styleframe/theme';
const s = styleframe();
const {
zIndex,
zIndexBase,
zIndexDropdown,
zIndexModal,
zIndexToast,
} = useZIndex(s, {
default: '@base',
base: '0',
dropdown: '1000',
modal: '2000',
toast: '3000',
});
export default s;
:root {
--z-index--base: 0;
--z-index--dropdown: 1000;
--z-index--modal: 2000;
--z-index--toast: 3000;
--z-index: var(--z-index--base);
}
The function creates variables for each stacking level you define. Each key in the object becomes a z-index variable with the prefix z-index--, and the export name is automatically converted to camelCase (e.g., dropdown → zIndexDropdown).
Pairing Z-Index with Box Shadow
Z-index and box shadow should work together — higher elevation elements should have both higher z-index and more prominent shadows:
import { styleframe } from 'styleframe';
import { useZIndex, useBoxShadow } from '@styleframe/theme';
const s = styleframe();
const { ref, selector } = s;
// Create z-index scale
const {
zIndexDropdown,
zIndexOverlay,
zIndexModal,
zIndexToast,
} = useZIndex(s);
// Create matching shadow scale
const {
boxShadowMd,
boxShadowLg,
boxShadowXl,
boxShadow2xl,
} = useBoxShadow(s);
// Pair elevation with stacking order
selector('.dropdown', {
zIndex: ref(zIndexDropdown),
boxShadow: ref(boxShadowMd),
});
selector('.overlay', {
zIndex: ref(zIndexOverlay),
});
selector('.modal', {
zIndex: ref(zIndexModal),
boxShadow: ref(boxShadowXl),
});
selector('.toast', {
zIndex: ref(zIndexToast),
boxShadow: ref(boxShadow2xl),
});
export default s;
:root {
--z-index--dropdown: 100;
--z-index--overlay: 300;
--z-index--modal: 400;
--z-index--toast: 600;
--box-shadow--md: 0 2px 4px oklch(var(--box-shadow-color, 0 0 0) / 0.16), 0 8px 16px -4px oklch(var(--box-shadow-color, 0 0 0) / 0.10);
--box-shadow--lg: 0 4px 8px oklch(var(--box-shadow-color, 0 0 0) / 0.18), 0 16px 24px -8px oklch(var(--box-shadow-color, 0 0 0) / 0.12);
--box-shadow--xl: 0 8px 12px oklch(var(--box-shadow-color, 0 0 0) / 0.20), 0 24px 48px -12px oklch(var(--box-shadow-color, 0 0 0) / 0.14);
--box-shadow--2xl: 0 12px 16px oklch(var(--box-shadow-color, 0 0 0) / 0.22), 0 32px 64px -16px oklch(var(--box-shadow-color, 0 0 0) / 0.16);
}
.dropdown {
z-index: var(--z-index--dropdown);
box-shadow: var(--box-shadow--md);
}
.overlay {
z-index: var(--z-index--overlay);
}
.modal {
z-index: var(--z-index--modal);
box-shadow: var(--box-shadow--xl);
}
.toast {
z-index: var(--z-index--toast);
box-shadow: var(--box-shadow--2xl);
}
Examples
Application Shell
Here's how to create a complete stacking system for a typical application layout:
import { styleframe } from 'styleframe';
import { useZIndex } from '@styleframe/theme';
const s = styleframe();
const { ref, selector } = s;
const {
zIndexSticky,
zIndexDropdown,
zIndexOverlay,
zIndexModal,
zIndexToast,
} = useZIndex(s);
// Sticky navigation
selector('.navbar', {
position: 'sticky',
top: '0',
zIndex: ref(zIndexSticky),
});
// Dropdown menus sit above sticky elements
selector('.dropdown-menu', {
position: 'absolute',
zIndex: ref(zIndexDropdown),
});
// Full-screen overlay behind modals
selector('.backdrop', {
position: 'fixed',
inset: '0',
zIndex: ref(zIndexOverlay),
});
// Modal sits above the overlay
selector('.modal', {
position: 'fixed',
zIndex: ref(zIndexModal),
});
// Toasts appear above everything
selector('.toast', {
position: 'fixed',
zIndex: ref(zIndexToast),
});
export default s;
:root {
--z-index--sticky: 200;
--z-index--dropdown: 100;
--z-index--overlay: 300;
--z-index--modal: 400;
--z-index--toast: 600;
}
.navbar {
position: sticky;
top: 0;
z-index: var(--z-index--sticky);
}
.dropdown-menu {
position: absolute;
z-index: var(--z-index--dropdown);
}
.backdrop {
position: fixed;
inset: 0;
z-index: var(--z-index--overlay);
}
.modal {
position: fixed;
z-index: var(--z-index--modal);
}
.toast {
position: fixed;
z-index: var(--z-index--toast);
}
Best Practices
- Always use tokens: Never write raw z-index numbers. Use the token scale so the entire team stays consistent.
- Match elevation with shadows: Higher z-index should correspond to higher box-shadow. This creates visual coherence.
- Use semantic names: Name tokens by purpose (modal, toast) not by number. This makes intent clear when reading code.
- Leave gaps between levels: The default 100-increment scale leaves room for intermediate values without reshuffling.
- Use
hidesparingly: Negative z-index (-1) can cause elements to become unreachable. Reserve it for decorative elements. - Mind stacking contexts: Remember that
z-indexonly works within the same stacking context. A modal inside az-index: 1container can't escape it. - Keep the scale small: Most applications need 5-8 z-index levels. More than that usually signals a stacking context issue.
FAQ
modal and toast communicate intent. When a developer reads zIndex: ref(zIndexModal), they immediately understand the stacking purpose. Raw numbers like z-index: 400 require tribal knowledge and are prone to conflicts when multiple developers pick different values for similar purposes.150 between dropdown and sticky). However, if you frequently need intermediate values, consider whether your DOM structure can be simplified. Flattening stacking contexts often removes the need for extra levels.position, opacity < 1, transform, filter, etc.) isolate their children's z-index from the outside. If a modal is inside a z-index: 1 container, it can never appear above a z-index: 2 sibling — no matter how high its own z-index is. Solution: keep high-elevation elements (modals, toasts) at the top of the DOM tree.xs with cards, md with popovers, lg with modals, and 2xl with toasts.max token (9999) is an escape hatch for truly exceptional cases — like a debug overlay or a critical system notification that must appear above everything. If you find yourself using it regularly, your stacking architecture likely needs refactoring.