Skip to content

Z-index

Z-index utilities control which element paints on top when elements overlap. NuvoUI has two sets: named levels for page layers (dropdowns, modals, toasts), which are tokens, and numbered keys for ordering siblings inside one component, which are plain numbers.

  • Named levels. z-dropdown, z-modal and the other levels output var(--nu-z-{level}). Use them for anything that must stack correctly against other parts of the page.
  • Numbered keys. z-10, z-50 and the other keys output the number itself (z-index: 10). Use them for local order inside a component, such as overlapping avatars.
  • Positioned elements only. z-index applies to positioned elements (relative, absolute, fixed, sticky) and to flex and grid items.

The levels and their default values, generated from the compiled core build:

KeyTokenValue
base--nu-z-base0
hover--nu-z-hover10
dropdown--nu-z-dropdown100
sticky--nu-z-sticky200
fixed--nu-z-fixed300
overlay--nu-z-overlay400
drawer--nu-z-drawer500
modal--nu-z-modal600
popover--nu-z-popover700
tooltip--nu-z-tooltip800
toast--nu-z-toast900
max--nu-z-max9999
Level Intended for
base Content in normal flow
hover An element lifted on hover, such as a card
dropdown Dropdown menus and select lists
sticky Sticky headers and table heads
fixed Fixed headers and navigation bars
overlay Backdrops behind drawers and modals
drawer Side drawers
modal Modal dialogs
popover Popovers, which can open from inside a modal
tooltip Tooltips; core tooltips use this level
toast Toast notifications, above everything else in the app
max Debugging tools and other elements that must be on top
z-dropdown (100)
z-modal (600)
z-sticky (200)
<div class="relative z-0 h-32">
<div class="absolute top-0 start-0 w-48 p-3 bg-surface border rounded shadow-md z-dropdown">z-dropdown (100)</div>
<div class="absolute top-6 start-12 w-48 p-3 bg-brand text-on-brand rounded shadow-md z-modal">z-modal (600)</div>
<div class="absolute top-12 start-24 w-48 p-3 bg-surface border rounded shadow-md z-sticky">z-sticky (200)</div>
</div>

The order in the source does not matter: the element with the higher level paints on top.

z-auto, z-0, z-10 to z-100 in steps of ten, z-200, z-500 and z-1000. They are not tokens; changing them means changing the configuration.

<div class="relative z-0 h-12">
<div class="absolute start-0 z-30 w-12 h-12 pill bg-brand border-2 border-white"></div>
<div class="absolute start-8 z-20 w-12 h-12 pill bg-purple border-2 border-white"></div>
<div class="absolute start-16 z-10 w-12 h-12 pill bg-pink border-2 border-white"></div>
</div>

Keep numbered keys inside a component that creates its own stacking context (for example with relative z-0), so they cannot compete with page layers.

A z-index only competes with elements in the same stacking context. An element with a positioned z-index, opacity below 1, a transform, a filter or isolation: isolate starts a new context, and nothing inside it can paint above an element outside it with a higher level. If a modal opened from inside a card stays under a header, move the modal out of the card in the DOM instead of raising its level.

Every z-index class has a responsive variant with an @{breakpoint} suffix: z-10@md, z-dropdown@lg. A variant applies from its breakpoint upwards.

<nav class="relative z-auto z-fixed@lg">Navigation</nav>
SuffixMedia query
@xs@media (min-width: 480px)
@sm@media (min-width: 640px)
@md@media (min-width: 768px)
@lg@media (min-width: 1024px)
@xl@media (min-width: 1280px)
@2xl@media (min-width: 1536px)

Z-index has no state variants.

  • sticky. The .sticky class sets z-index: 999 inside :where(), so any z-index class on the same element overrides it: sticky z-sticky. See Position.
  • elevation-*. Elevation classes set a z-index from 1 to 5. Z-index classes come later in the stylesheet, so elevation-3 z-dropdown uses the dropdown level. See Shadows.
@use "@nuvoui/core" as *;
.dialog {
@include apply(fixed, z-index(modal));
}
.avatar-stack__item {
@include apply(relative, z-index(10));
}
.dialog {
position: fixed;
z-index: var(--nu-z-modal);
}
.avatar-stack__item {
position: relative;
z-index: 10;
}

z-index() accepts a level name (gives the token), a numbered key (gives the number), any integer, auto, calc() or var(). Each level also has a shortcut mixin of the same name as the class: z-base, z-hover, z-dropdown, z-sticky, z-fixed, z-overlay, z-drawer, z-modal, z-popover, z-tooltip, z-toast, z-max, and z-auto.

.toast {
@include apply(fixed, z-toast);
}
.layer {
@include apply(z-index(15));
}

Only named levels are tokens.

.command-palette {
z-index: token(z-popover);
}

$z-index-levels holds the named levels and $z-indexes the numbered keys. Each replaces the default map; it is not merged. Core tooltips read the tooltip level, so keep it when you use tooltips.

@use "@nuvoui/core" with (
$generate-utility-classes: true,
$z-index-levels: ("base": 0, "hover": 10, "dropdown": 1000, "sticky": 1100, "fixed": 1200, "overlay": 1300, "drawer": 1400, "modal": 1500, "popover": 1600, "tooltip": 1700, "toast": 1800, "max": 9999)
);

A new key adds a class, a token and a valid z-index() argument: adding "banner": 1250 gives .z-banner, --nu-z-banner and z-index(banner). The shortcut mixins exist only for the default level names.

Input Error
z-index(huge) Unknown z-index: huge. Use a key (auto, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 500, 1000), a level (base, hover, dropdown, sticky, fixed, overlay, drawer, modal, popover, tooltip, toast, max), an integer, auto, calc() or var().
z-index(1.5) Unknown z-index: 1.5. Use a key (...), a level (...), an integer, auto, calc() or var().
token(z-10) token(z-10): unknown token. Close matches: z-base, z-hover, z-dropdown, ... (numbered keys are not tokens)
Variable Description
--nu-z-{level} One token per key of $z-index-levels. Fixed across themes. See Named levels.

Numbered keys are emitted as plain numbers.

Generated from the compiled core build. Responsive variants are not listed.

Named levels (12 rules)
SelectorDeclarationsDefault value
.z-basez-index: var(--nu-z-base)0
.z-hoverz-index: var(--nu-z-hover)10
.z-dropdownz-index: var(--nu-z-dropdown)100
.z-stickyz-index: var(--nu-z-sticky)200
.z-fixedz-index: var(--nu-z-fixed)300
.z-overlayz-index: var(--nu-z-overlay)400
.z-drawerz-index: var(--nu-z-drawer)500
.z-modalz-index: var(--nu-z-modal)600
.z-popoverz-index: var(--nu-z-popover)700
.z-tooltipz-index: var(--nu-z-tooltip)800
.z-toastz-index: var(--nu-z-toast)900
.z-maxz-index: var(--nu-z-max)9999
Numbered keys (15 rules)
SelectorDeclarationsDefault value
.z-autoz-index: autoauto
.z-0z-index: 00
.z-10z-index: 1010
.z-20z-index: 2020
.z-30z-index: 3030
.z-40z-index: 4040
.z-50z-index: 5050
.z-60z-index: 6060
.z-70z-index: 7070
.z-80z-index: 8080
.z-90z-index: 9090
.z-100z-index: 100100
.z-200z-index: 200200
.z-500z-index: 500500
.z-1000z-index: 10001000
  • Layers and focus. Raising a modal’s z-index makes it appear on top but does not stop keyboard focus from reaching content behind it. Use the dialog element with showModal(), or inert on the rest of the page.
  • Do not cover focused content. Sticky and fixed layers can hide the element that has focus (WCAG 2.2, 2.4.11 Focus Not Obscured). Leave space with scroll-padding-top, or keep such layers small.
  • Top layer. A dialog opened with showModal() and an element shown with the Popover API render in the browser’s top layer, above every z-index. Use z-index levels for everything else.
  • RTL. Z-index has no direction.