Z-Index Guideline

Why This Guideline?

Uncontrolled z-indexes lead to hard-to-debug overlapping issues. This guideline defines mandatory layers for all UI elements and prevents "z-index wars" between components.


Layer Overview

The entire UI is divided into six core layers. Each layer has a dedicated z-index range and defines a specific level of visual hierarchy. Every element on the page belongs to exactly one of these layers.

Base (0–9)

The base layer contains all regular page elements that are part of the normal document flow. This is the default layer for everything that does not require special stacking behavior. Elements here may have decorative backgrounds (e.g. gradients, background images), be slightly elevated (e.g. a card with a shadow) or have short-lived input overlays (e.g. a combobox), but they never visually compete with navigation, dialogs or notifications.

Rule of thumb: If it scrolls with the page and does not block the rest of the UI, it belongs in the Base layer.

Sticky (10–19)

The sticky layer contains persistently visible elements that remain on screen while the user scrolls. These elements need to stay above all base layer content at all times. The sticky layer may also contain its own sub-elements such as input overlays or raised elements (e.g. a search combobox inside a navbar).

Rule of thumb: If it sticks to the viewport and is always visible, it belongs in the Sticky layer.

Panel (20–29)

The panel layer contains large overlay elements that slide in or expand over the page content, such as off-canvas navigations, burger menus or mega-menus. Unlike modals, panels are typically triggered by navigation interactions and do not necessarily require the user to explicitly dismiss them before continuing. They sit above sticky elements but below modal dialogs.

Rule of thumb: If it slides or expands over the page content as part of navigation, it belongs in the Panel layer.

The modal layer contains dialogs and overlays that intentionally block interaction with the rest of the UI. A backdrop is used to visually separate the modal from the content below. Elements inside a modal (e.g. a combobox) use modal-specific sub-layer tokens to stay within the modal's stacking context.

Rule of thumb: If it requires the user's attention and blocks the rest of the UI, it belongs in the Modal layer.

Notification (40–49)

The notification layer contains short-lived messages that need to be visible regardless of what is currently open on the page — including modals. It sits above all primary content layers but below popovers, so a popover triggered from within a notification (e.g. a help-tooltip on a snackbar) can still appear on top.

Rule of thumb: If it's a system-generated, short-lived message, it belongs in the Notification layer.

Popover (50–59)

The popover layer contains transient elements anchored to a trigger and invoked by a direct user interaction (right-click, hover, click, keyboard shortcut). Despite the name, this layer covers all anchored floating elements — including context menus, tooltips, popovers, dropdown menus rendered into a portal and drag previews. Because these elements are often triggered from inside other layers (e.g. a context menu inside a modal, a tooltip on a toast), they must sit above everything else. Nothing except z-force should ever appear above this layer.

Rule of thumb: If it's a transient element triggered by a direct user interaction and anchored to a trigger, it belongs in the Popover layer.


Layer System

Layer Sub-Layer Class Value Description Examples
Base Backdrop z-base-backdrop 4 Decorative background elements Gradients, Background Images
z-base 5 Regular page elements Cards, Sections
Raised z-base-raised 6 Slightly elevated elements within the page flow Floating Buttons, Tooltips in Content
Input z-base-input 7 Short-lived overlays of input components on the page Combobox, Select, Datepicker
Sticky Backdrop z-sticky-backdrop 9 Semi-transparent background behind a sticky element Sticky Backdrop
z-sticky 10 Persistently visible navigation elements Navbar, Sidebar
Raised z-sticky-raised 11 Slightly elevated elements within a sticky context Dropdown Menu in Navbar
Input z-sticky-input 12 Short-lived overlays of input components inside a sticky element Combobox inside a Navbar
Panel Backdrop z-panel-backdrop 19 Semi-transparent background behind a panel Burger Menu Backdrop
z-panel 20 Large overlay navigation elements Burger Menu, Off-Canvas Nav, Mega-Menu
Raised z-panel-raised 21 Slightly elevated elements within a panel
Input z-panel-input 22 Short-lived overlays of input components inside a panel Combobox inside a Burger Menu
Modal Backdrop z-modal-backdrop 29 Semi-transparent background behind a modal Modal Backdrop, Drawer Backdrop
z-modal 30 Modal dialogs Dialog, Confirmation, Lightbox
Raised z-modal-raised 31 Slightly elevated elements within a modal Floating Buttons inside a Modal
Input z-modal-input 32 Short-lived overlays of input components inside a modal Combobox inside a Modal
Notification Backdrop z-notification-backdrop 39 Semi-transparent background behind a notification Notification Backdrop
z-notification 40 Short-lived notifications Toast, Snackbar
Raised z-notification-raised 41 Slightly elevated elements within a notification
Input z-notification-input 42 Short-lived overlays of input components inside a notification Combobox inside a Notification
Popover Backdrop z-popover-backdrop 49 Semi-transparent background behind a popover Popover Backdrop (rare)
z-popover 50 Anchored, user-triggered floating elements Context Menu, Tooltip, Popover, Dropdown
Raised z-popover-raised 51 Slightly elevated elements within a popover Submenu of a Context Menu
Input z-popover-input 52 Short-lived overlays of input components inside a popover Combobox inside a Popover
Force z-force 99 Only allowed with justification and ticket

Implementation

Tailwind Projects

The tokens are defined in tailwind.config.js and are mandatory for all developers. Changes only after team alignment.

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      zIndex: {
        // Base
        'base-backdrop':           '4',
        'base':                    '5',
        'base-raised':             '6',
        'base-input':              '7',

        // Sticky
        'sticky-backdrop':         '9',
        'sticky':                  '10',
        'sticky-raised':           '11',
        'sticky-input':            '12',

        // Panel
        'panel-backdrop':          '19',
        'panel':                   '20',
        'panel-raised':            '21',
        'panel-input':             '22',

        // Modal
        'modal-backdrop':          '29',
        'modal':                   '30',
        'modal-raised':            '31',
        'modal-input':             '32',

        // Notification
        'notification-backdrop':   '39',
        'notification':            '40',
        'notification-raised':     '41',
        'notification-input':      '42',

        // Popover
        'popover-backdrop':        '49',
        'popover':                 '50',
        'popover-raised':          '51',
        'popover-input':           '52',

        // Exceptions
        'force':                   '99',
      }
    }
  }
}

Use the Tailwind classes directly in your markup:

<!-- ✅ Tailwind -->
<div class="z-modal isolate">
  <div class="z-modal-input">
    ...
  </div>
</div>

Non-Tailwind Projects

Use the numeric values from the layer table directly as CSS custom properties or inline styles. We recommend defining them as CSS custom properties in a central location:

:root {
  /* Base */
  --z-base-backdrop:           4;
  --z-base:                    5;
  --z-base-raised:             6;
  --z-base-input:              7;

  /* Sticky */
  --z-sticky-backdrop:         9;
  --z-sticky:                  10;
  --z-sticky-raised:           11;
  --z-sticky-input:            12;

  /* Panel */
  --z-panel-backdrop:          19;
  --z-panel:                   20;
  --z-panel-raised:            21;
  --z-panel-input:             22;

  /* Modal */
  --z-modal-backdrop:          29;
  --z-modal:                   30;
  --z-modal-raised:            31;
  --z-modal-input:             32;

  /* Notification */
  --z-notification-backdrop:   39;
  --z-notification:            40;
  --z-notification-raised:     41;
  --z-notification-input:      42;

  /* Popover */
  --z-popover-backdrop:        49;
  --z-popover:                 50;
  --z-popover-raised:          51;
  --z-popover-input:           52;

  /* Exceptions */
  --z-force:                   99;
}
/* ✅ Non-Tailwind */
.modal {
  z-index: var(--z-modal);
  isolation: isolate;
}

.modal .combobox {
  z-index: var(--z-modal-input);
}

Rules

✅ Required

  • Always use a token from the layer table — either as a Tailwind class or CSS custom property
  • Components that act as containers (Modal, Drawer, Panel) must create an isolated stacking context — see Stacking Context below
  • Popovers, Context Menus and Tooltips should be rendered into a portal (e.g. document.body) to avoid being clipped by parent stacking contexts or overflow: hidden
  • When using z-force / --z-force: add a comment with justification and ticket number directly in the code
<!-- ✅ Correct (Tailwind) -->
<div class="z-modal isolate">
  <div class="z-modal-input">  <!-- Combobox inside a Modal -->
    ...
  </div>
</div>

<!-- ✅ Correct with justification -->
<div class="z-force"> <!-- TODO: TICKET-123 – Workaround until legacy widget is replaced -->
/* ✅ Correct (Non-Tailwind) */
.modal {
  z-index: var(--z-modal);
  isolation: isolate;
}

❌ Forbidden

<!-- Arbitrary values (Tailwind) -->
<div class="z-[400]">
<div class="z-[9999]">
/* Magic numbers */
.my-element { z-index: 9999; }

/* z-index without position or display — has no effect and causes confusion */
.my-element { z-index: 30; } /* missing: position: relative/absolute/fixed/sticky */

Stacking Context

What is a Stacking Context?

A stacking context is an isolated layer in the browser's rendering model. Z-indexes inside a stacking context only apply relative to their siblings within that same context — not to the rest of the page. This means an element with z-index: 9999 inside a stacking context will never appear above an element outside of it that has a lower z-index.

This is the most common reason why a z-index seems to "not work".

How to create a Stacking Context

Any container that acts as a layer root (Modal, Panel, Drawer) must explicitly create a stacking context using isolation: isolate:

<!-- Tailwind -->
<div class="z-modal isolate"> ... </div>
/* Non-Tailwind */
.modal {
  z-index: var(--z-modal);
  isolation: isolate;
}

This ensures that all child elements — regardless of their z-index — can never accidentally overlap elements outside their container.

Important: Popovers must not be rendered inside a container with isolation: isolate — they would be trapped inside that stacking context and could not appear above other layers. Always render popovers, tooltips and context menus into a portal (e.g. document.body).

What else creates a Stacking Context?

Besides isolation: isolate the browser automatically creates a new stacking context for elements with certain CSS properties. This can cause unexpected stacking behavior if not accounted for:

CSS Property Creates new Stacking Context
isolation: isolate Yes (intentional)
transform Yes
opacity < 1 Yes
filter Yes
will-change Yes

If a z-index seems to not work, check whether any parent element has one of these properties set unintentionally.

Choosing the right token inside a Container

Always use the sub-layer tokens that match the surrounding context:

<!-- Standalone Combobox on the page -->
<div class="relative z-base-input">
  ...
</div>

<!-- Combobox inside a Sticky Navbar -->
<div class="sticky z-sticky isolate">           <!-- Navbar -->
  <div class="relative z-sticky-input">         <!-- Combobox -->
    ...
  </div>
</div>

<!-- Burger Menu -->
<div class="fixed z-panel-backdrop">            <!-- Backdrop -->
  ...
</div>
<div class="fixed z-panel isolate">             <!-- Panel -->
  <div class="relative z-panel-input">          <!-- Combobox inside Burger Menu -->
    ...
  </div>
</div>

<!-- Combobox inside a Modal -->
<div class="fixed z-modal-backdrop">            <!-- Backdrop -->
  ...
</div>
<div class="fixed z-modal isolate">             <!-- Modal -->
  <div class="relative z-modal-input">          <!-- Combobox -->
    ...
  </div>
</div>

<!-- Context Menu (rendered into a portal, e.g. document.body) -->
<div class="fixed z-popover">                   <!-- Context Menu -->
  <ul>...</ul>
</div>

<!-- Tooltip on a Toast (rendered into a portal) -->
<div class="fixed z-popover">                   <!-- Tooltip floats above the toast -->
  ...
</div>

If additional nested contexts are needed (e.g. a Modal triggered from inside a Panel), please discuss with the team before adding new tokens on your own.


Quick Reference: Which Layer do I need?

Element Layer / Token
Tooltip rendered inline (no portal) z-base-raised
Tooltip rendered into body (portal) z-popover
Combobox / Select dropdown in a form z-base-input
Dropdown menu inside a navbar z-sticky-input
Dropdown menu rendered into body (portal) z-popover
Context menu (right-click) z-popover
Toast / Snackbar z-notification
Confirmation dialog z-modal
Off-canvas navigation z-panel
Drag preview / drag ghost z-popover

Debugging

Useful for debugging: Stacking Context Inspector (Chrome Extension)


Questions / Changes

New use cases that don't fit into the existing tokens → open an issue or post in the frontend channel. Tokens are not to be added or modified independently.