CSS3 & Responsive Design

Z-Index & Stacking Contexts

25 min Lesson 22 of 60

Introduction to Stacking and Z-Index

When you build web pages, elements do not just exist in two dimensions (horizontal and vertical). They also exist along a third axis -- the z-axis -- which controls which elements appear in front of or behind other elements. The z-index property is CSS's primary tool for controlling this stacking order, but it is one of the most commonly misunderstood properties in all of CSS. Many developers encounter unexpected behavior with z-index, write values like z-index: 99999 in frustration, and never fully understand why their elements are not stacking correctly.

The root of the confusion is the concept of stacking contexts. A stacking context is a three-dimensional conceptualization of HTML elements along an imaginary z-axis. Elements within a stacking context are stacked according to specific rules, and -- crucially -- a stacking context forms a self-contained unit. Elements inside a stacking context can never appear in front of or behind elements in a sibling stacking context, regardless of their z-index values. Understanding stacking contexts is the key to mastering z-index.

In this lesson, we will cover how the default stacking order works without z-index, how z-index controls stacking on positioned elements, what creates stacking contexts and why they matter, common properties that unexpectedly create stacking contexts, why z-index: 9999 is a code smell, how to manage z-index values systematically with CSS custom properties, the isolation: isolate property, how to debug stacking issues with DevTools, and practical patterns for modals, dropdowns, and tooltips.

Default Stacking Order (Without Z-Index)

Before we even consider z-index, it is important to understand how browsers naturally stack elements when no explicit stacking is specified. The browser follows a specific painting order defined in the CSS specification. Elements painted later appear on top of elements painted earlier.

The default stacking order from back to front is:

  1. Background and borders of the root element (the <html> element)
  2. Non-positioned block-level elements in the order they appear in the HTML (normal flow), such as <div>, <p>, <section>
  3. Floating elements -- floated elements paint above non-positioned block elements
  4. Inline elements -- inline content (text, <span>, inline images) paints above floats
  5. Positioned elements with position: relative, absolute, fixed, or sticky (and z-index: auto) -- in the order they appear in the HTML

Default Stacking Order Example

<div class="container">
    <div class="block-one">Block Element 1</div>
    <div class="block-two">Block Element 2</div>
    <div class="positioned">Positioned Element</div>
    <div class="floated">Floated Element</div>
</div>

/* CSS */
.block-one {
    background: lightcoral;
    /* No position -- normal flow, painted first */
}

.block-two {
    background: lightblue;
    margin-top: -20px; /* Overlaps block-one */
    /* No position -- still normal flow, painted after block-one */
    /* So block-two appears ON TOP of block-one */
}

.positioned {
    position: relative;
    top: -40px;
    background: lightgreen;
    /* Positioned -- painted ABOVE all non-positioned and floated elements */
}

.floated {
    float: left;
    background: lightyellow;
    margin-top: -60px;
    /* Floated -- painted above non-positioned blocks, below positioned */
}
Note: The key takeaway from the default stacking order is that positioned elements always paint on top of non-positioned elements, regardless of their order in the HTML source. This is why adding position: relative to an element (even without any offset values) can change its visual stacking. A positioned element without a z-index (z-index: auto) will still appear above any non-positioned element.

Z-Index: Controlling Stacking Order

The z-index property allows you to explicitly control the stacking order of elements. However, there is a critical rule that trips up many developers: z-index only works on positioned elements. An element must have its position property set to relative, absolute, fixed, or sticky for z-index to have any effect. On a position: static element (the default), the z-index property is completely ignored.

Z-Index Basic Usage

/* z-index has NO effect on static elements */
.static-element {
    position: static; /* default */
    z-index: 100;     /* IGNORED -- has no effect */
}

/* z-index WORKS on positioned elements */
.relative-element {
    position: relative;
    z-index: 10;      /* Works -- element stacks above z-index: auto */
}

.absolute-element {
    position: absolute;
    z-index: 5;       /* Works -- stacks below the relative element above */
}

.fixed-element {
    position: fixed;
    z-index: 20;      /* Works -- stacks above both elements above */
}

.sticky-element {
    position: sticky;
    top: 0;
    z-index: 15;      /* Works -- stacks between fixed and relative above */
}

Z-Index Values: Positive, Negative, and Auto

The z-index property accepts integer values (positive, negative, or zero) and the keyword auto. Understanding the difference between these values is essential for predictable stacking behavior.

Z-Index Value Types

/* auto -- the default value */
.auto-z {
    position: relative;
    z-index: auto;
    /* Does NOT create a new stacking context */
    /* Element participates in its parent's stacking context */
    /* Equivalent stacking order to z-index: 0 in terms of visual layer */
}

/* Positive z-index -- stacks above z-index: auto and z-index: 0 elements */
.positive-z {
    position: relative;
    z-index: 10;
    /* Creates a new stacking context */
    /* Higher positive values stack above lower positive values */
}

/* z-index: 0 -- same visual layer as auto, BUT creates a stacking context */
.zero-z {
    position: relative;
    z-index: 0;
    /* IMPORTANT: Unlike auto, z-index: 0 CREATES a new stacking context */
    /* Visually at the same layer as auto */
}

/* Negative z-index -- stacks below the parent element's background */
.negative-z {
    position: relative;
    z-index: -1;
    /* Creates a new stacking context */
    /* Renders BEHIND the parent's background and content */
    /* But still above the parent's stacking context root background */
}

/* Demonstration of stacking order */
.behind-parent {
    position: absolute;
    z-index: -1;
    /* This element appears behind its positioned parent */
    /* Useful for decorative backgrounds and pseudo-elements */
}

.parent {
    position: relative;
    /* z-index: auto (default) -- no stacking context */
    /* Child with z-index: -1 appears behind this parent */
}

.above-siblings {
    position: relative;
    z-index: 1;
    /* Appears above all siblings with z-index: auto or z-index: 0 */
}
Tip: There is a subtle but important difference between z-index: auto and z-index: 0. While they result in the same visual stacking position, z-index: 0 creates a new stacking context while z-index: auto does not. This distinction matters enormously when you have nested elements with their own z-index values. When in doubt, prefer z-index: auto (or just omit z-index) unless you specifically need a new stacking context.

Stacking Contexts: The Key to Understanding Z-Index

A stacking context is a hierarchical grouping of elements along the z-axis. Think of it as a "layer group" -- all elements within a stacking context are stacked relative to each other, and the entire group is then stacked as a unit relative to other stacking contexts. This is the single most important concept for understanding why z-index sometimes does not work as expected.

The root element (<html>) always creates the initial stacking context. Any element that creates a new stacking context becomes the "root" of its own miniature stacking world. Elements inside that context can only compete for stacking position against other elements in the same context -- they cannot escape their parent stacking context, no matter how high their z-index is.

Stacking Context Demonstration

<!-- HTML -->
<div class="parent-a">
    <div class="child-a">Child A (z-index: 999)</div>
</div>
<div class="parent-b">
    <div class="child-b">Child B (z-index: 1)</div>
</div>

/* CSS */
.parent-a {
    position: relative;
    z-index: 1;    /* Creates stacking context with z-index 1 */
    background: lightcoral;
    padding: 20px;
}

.child-a {
    position: relative;
    z-index: 999;  /* High z-index, but trapped in parent-a's context */
    background: red;
    padding: 10px;
}

.parent-b {
    position: relative;
    z-index: 2;    /* Creates stacking context with z-index 2 */
    background: lightblue;
    padding: 20px;
    margin-top: -30px; /* Overlaps with parent-a */
}

.child-b {
    position: relative;
    z-index: 1;    /* Low z-index, but in parent-b's context */
    background: blue;
    color: white;
    padding: 10px;
}

/* RESULT: child-b (z-index: 1) appears ABOVE child-a (z-index: 999)!
   Why? Because parent-b (z-index: 2) stacks above parent-a (z-index: 1).
   The entire parent-b stacking context, including all its children,
   is above the entire parent-a stacking context.
   child-a's z-index: 999 only matters WITHIN parent-a. */
Warning: This is the number one source of z-index confusion. If an element with z-index: 9999 is not appearing on top, the problem is almost certainly that its parent (or an ancestor) has created a stacking context with a lower z-index than a sibling stacking context. The fix is never to increase z-index further -- it is to understand and restructure the stacking contexts. Look up the DOM tree to find which ancestor is creating the problematic stacking context.

What Creates a Stacking Context

Many CSS properties create new stacking contexts, and several of them are not obvious at all. This is another major source of unexpected z-index behavior. You might add a seemingly innocent CSS property to an element and suddenly find that the stacking order of its descendants has changed. Here is a comprehensive list of conditions that create new stacking contexts:

The Classic Triggers

Classic Stacking Context Triggers

/* 1. Root element -- always creates the initial stacking context */
html {
    /* This is automatically a stacking context */
}

/* 2. Positioned element with z-index other than auto */
.positioned-with-z {
    position: relative; /* or absolute, fixed, sticky */
    z-index: 1;         /* Any integer value, including 0 */
    /* Creates a stacking context */
}

/* 3. Positioned element with z-index: auto does NOT create one */
.positioned-without-z {
    position: relative;
    z-index: auto;      /* Default -- NO stacking context */
}

Properties That Unexpectedly Create Stacking Contexts

The following CSS properties create stacking contexts even without any position or z-index being set. These are the "silent" stacking context creators that often cause mysterious z-index bugs.

Unexpected Stacking Context Triggers

/* opacity less than 1 */
.transparent {
    opacity: 0.99;
    /* Creates a stacking context! Even 0.99 triggers it. */
    /* This is why adding opacity to a parent can break z-index of children */
}

/* transform (any value other than none) */
.transformed {
    transform: translateX(0);
    /* Creates a stacking context! Even an identity transform like this */
}

.rotated {
    transform: rotate(0deg);
    /* Creates a stacking context! */
}

/* filter (any value other than none) */
.filtered {
    filter: blur(0px);
    /* Creates a stacking context! */
}

.brightness {
    filter: brightness(1);
    /* Creates a stacking context! Even though it changes nothing visually */
}

/* backdrop-filter */
.backdrop {
    backdrop-filter: blur(10px);
    /* Creates a stacking context */
}

/* mix-blend-mode (any value other than normal) */
.blended {
    mix-blend-mode: multiply;
    /* Creates a stacking context */
}

/* isolation: isolate */
.isolated {
    isolation: isolate;
    /* Creates a stacking context -- this is its ENTIRE purpose */
}

/* will-change (when specifying properties that would create stacking context) */
.optimized {
    will-change: transform;
    /* Creates a stacking context even before transform is applied */
}

/* position: fixed or sticky (always creates stacking context in modern browsers) */
.fixed {
    position: fixed;
    /* Always creates a stacking context, even without z-index */
}

.sticky {
    position: sticky;
    top: 0;
    /* Always creates a stacking context, even without z-index */
}

/* contain: layout, paint, or strict */
.contained {
    contain: paint;
    /* Creates a stacking context */
}

/* CSS Regions, clip-path, mask, and others also create stacking contexts */
.clipped {
    clip-path: circle(50%);
    /* Creates a stacking context */
}

.masked {
    mask: url(mask.svg);
    /* Creates a stacking context */
}
Warning: The most common accidental stacking context creators are transform, opacity (less than 1), and filter. If you add transform: translateY(-5px) to a card's hover effect, that card now creates a stacking context. Any child elements with negative z-index that previously rendered behind the card will now be trapped inside the card's stacking context. Similarly, adding opacity: 0.99 as a performance hack or filter: drop-shadow() for shadow effects will create stacking contexts and potentially break your z-index hierarchy.

Stacking Order Within a Context

Within a single stacking context, elements are painted in the following order from back to front. Understanding this order explains many stacking behaviors that seem mysterious at first glance.

Complete Stacking Order Within a Context

/*
   Painting order within a stacking context (back to front):

   1. The stacking context's own background and borders
   2. Child elements with negative z-index (most negative first)
   3. Non-positioned, non-floated block elements (in DOM order)
   4. Floated elements (in DOM order)
   5. Inline elements (in DOM order)
   6. Positioned elements with z-index: auto or z-index: 0 (in DOM order)
   7. Child elements with positive z-index (lowest first)
*/

/* Practical demonstration */
.stacking-context-root {
    position: relative;
    z-index: 0; /* Creates a stacking context */
    background: #f0f0f0;
    padding: 40px;
}

/* Layer 1 (behind everything): negative z-index */
.background-decoration {
    position: absolute;
    z-index: -1;
    /* Paints above the parent's background but below its content */
    background: rgba(52, 152, 219, 0.1);
    top: 10px;
    left: 10px;
    right: 10px;
    bottom: 10px;
    border-radius: 8px;
}

/* Layer 2: non-positioned block (normal flow) */
.normal-block {
    background: lightcoral;
    padding: 10px;
    /* No position, no z-index -- paints in normal flow */
}

/* Layer 3: floated element */
.floated-box {
    float: left;
    width: 100px;
    height: 100px;
    background: lightyellow;
    /* Paints above non-positioned blocks */
}

/* Layer 4: positioned with z-index: auto */
.positioned-auto {
    position: relative;
    /* z-index: auto (default) */
    background: lightgreen;
    padding: 10px;
    /* Paints above floats */
}

/* Layer 5: positive z-index (low) */
.low-z {
    position: relative;
    z-index: 1;
    background: lightskyblue;
    padding: 10px;
}

/* Layer 6: positive z-index (high) */
.high-z {
    position: relative;
    z-index: 10;
    background: plum;
    padding: 10px;
    /* Paints on top of everything in this stacking context */
}

Why z-index: 9999 is a Code Smell

You have probably seen CSS codebases littered with values like z-index: 9999, z-index: 99999, or even z-index: 2147483647 (the maximum 32-bit integer). These are almost always symptoms of not understanding stacking contexts, and they create a maintenance nightmare. Here is why arbitrarily high z-index values are problematic and what to do instead.

The Z-Index Arms Race (Anti-Pattern)

/* The problem: z-index arms race */
.header {
    position: sticky;
    top: 0;
    z-index: 100;      /* "High enough" for a header */
}

.dropdown {
    position: absolute;
    z-index: 200;      /* Needs to be above header, so 200 */
}

.modal-overlay {
    position: fixed;
    z-index: 999;      /* Needs to be above everything, so 999 */
}

.modal {
    position: fixed;
    z-index: 1000;     /* Must be above the overlay */
}

.toast-notification {
    position: fixed;
    z-index: 9999;     /* Must be above the modal! */
}

.tooltip {
    position: absolute;
    z-index: 99999;    /* Must be above EVERYTHING! */
}

/* Six months later, someone adds: */
.cookie-banner {
    position: fixed;
    z-index: 999999;   /* Must be above the tooltip... */
}

/* This is completely unmaintainable! */

Managing Z-Index with CSS Custom Properties

The solution to the z-index arms race is to define a clear, documented z-index scale using CSS custom properties (variables). This creates a single source of truth for all z-index values in your project, making it easy to understand the intended stacking hierarchy and modify it when needed.

Z-Index Scale with CSS Custom Properties

/* Define a clear z-index scale */
:root {
    --z-below:      -1;
    --z-normal:      0;
    --z-dropdown:   10;
    --z-sticky:     20;
    --z-fixed:      30;
    --z-overlay:    40;
    --z-modal:      50;
    --z-popover:    60;
    --z-tooltip:    70;
    --z-toast:      80;
    --z-max:        90;
}

/* Use the scale consistently */
.header {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
}

.dropdown-menu {
    position: absolute;
    z-index: var(--z-dropdown);
}

.sidebar-fixed {
    position: fixed;
    z-index: var(--z-fixed);
}

.modal-overlay {
    position: fixed;
    z-index: var(--z-overlay);
}

.modal-dialog {
    position: fixed;
    z-index: var(--z-modal);
}

.popover {
    position: absolute;
    z-index: var(--z-popover);
}

.tooltip {
    position: absolute;
    z-index: var(--z-tooltip);
}

.toast {
    position: fixed;
    z-index: var(--z-toast);
}

/* Benefits:
   1. Every z-index in the project is documented in one place
   2. The hierarchy is clear: tooltips > modals > dropdowns > headers
   3. No more guessing -- just check the scale
   4. Easy to adjust if needed: change one variable, everything updates
   5. Gaps between values (10, 20, 30) allow inserting new layers later
*/
Tip: Use increments of 10 in your z-index scale rather than 1, 2, 3. This leaves room to insert new layers between existing ones without having to renumber everything. For example, if you later need something between the dropdown (10) and the sticky header (20), you can use 15 without changing any existing values. The values themselves do not matter -- only their relative order matters.

isolation: isolate -- The Stacking Context Tool

The isolation property was introduced specifically to give developers a clean way to create new stacking contexts without any visual side effects. Its sole purpose is to create a stacking context. Unlike using position: relative; z-index: 0, opacity: 0.99, or transform: translateZ(0) (all common hacks to create stacking contexts), isolation: isolate does exactly one thing: it creates a new stacking context with no other effects on layout, appearance, or performance.

Using isolation: isolate

/* The problem: a child with z-index: -1 escapes its parent */
.card {
    position: relative;
    background: white;
    /* z-index: auto (default) -- NOT a stacking context */
}

.card-decoration {
    position: absolute;
    z-index: -1;
    top: -10px;
    left: -10px;
    right: -10px;
    bottom: -10px;
    background: linear-gradient(135deg, #667eea, #764ba2);
    border-radius: 16px;
    /* BUG: This goes behind the PARENT's parent, not just the card!
       Because the card is not a stacking context, z-index: -1
       escapes up to the nearest ancestor stacking context. */
}

/* Solution 1: The old hack -- position + z-index: 0 */
.card-fix-old {
    position: relative;
    z-index: 0; /* Creates stacking context, but also sets z-index to 0 */
}

/* Solution 2: The clean approach -- isolation: isolate */
.card-fix-clean {
    isolation: isolate;
    /* Creates stacking context with NO side effects!
       No need for position: relative
       No z-index value is set
       No visual changes
       The child's z-index: -1 now stays behind the card, not escaping */
}

/* Real-world example: component isolation */
.widget {
    isolation: isolate;
    /* All z-index values inside this widget are now contained.
       They cannot interfere with elements outside the widget.
       And external z-index values cannot reach inside either. */
}

/* Another example: preventing mix-blend-mode from bleeding */
.section-with-blending {
    isolation: isolate;
    /* mix-blend-mode on children will blend with the section's
       background, not with elements behind the section */
}
Note: isolation: isolate is the recommended way to create a stacking context when you need one purely for containment purposes. It is semantically clear (anyone reading the code understands you are intentionally creating an isolation boundary), has no side effects, and works without needing to set a position or z-index. Use it liberally in component-based architectures to prevent z-index leaking between components.

Debugging Stacking with DevTools

When you encounter z-index issues, browser DevTools are your best friend. Both Chrome DevTools and Firefox DevTools provide tools to inspect and visualize stacking contexts. Here is how to debug stacking problems systematically.

Debugging Steps and Techniques

/* Step 1: Identify the elements involved */
/* In DevTools, inspect both the element you want on top
   and the element that is blocking it */

/* Step 2: Check if z-index is being applied */
/* In the Computed tab, look for:
   - position: Is it anything other than static?
   - z-index: Is the computed value what you expect?
   - If position is static, z-index will show "auto" regardless
     of what you set in your CSS */

/* Step 3: Find the stacking context boundaries */
/* Walk up the DOM tree from the problematic element.
   For each ancestor, check if it creates a stacking context.
   Look for these properties in the Computed tab:
   - position + z-index (not auto)
   - opacity < 1
   - transform (not none)
   - filter (not none)
   - isolation: isolate
   - will-change: transform/opacity/filter
   - position: fixed or sticky
   - mix-blend-mode (not normal)
   - contain: layout/paint/strict */

/* Step 4: Visualize the hierarchy */
/* You can add temporary outlines to see stacking context boundaries: */
.debug-stacking-context {
    outline: 3px solid red !important;
}

/* Step 5: Use Firefox's 3D view (Tilting) */
/* Firefox DevTools has a unique 3D view that shows
   the stacking order visually. Access it via:
   DevTools > Inspector > three-dot menu > Show split console
   Then use the 3D View panel */

/* Step 6: Common fixes */

/* Fix A: Remove accidental stacking context on ancestor */
.ancestor-that-breaks-things {
    /* Remove or adjust: */
    /* transform: none; */
    /* opacity: 1; */
    /* filter: none; */
}

/* Fix B: Create intentional stacking context on the right element */
.component-root {
    isolation: isolate;
    /* Contains all z-index within this component */
}

/* Fix C: Move the element higher in the DOM hierarchy */
/* If an element needs to be above everything, consider
   moving it to be a direct child of <body> rather than
   being deeply nested inside a stacking context */

/* Fix D: Use a portal pattern (common in React/Vue) */
/* Render modals and tooltips at the top level of the DOM
   to avoid stacking context issues entirely */
Tip: In Chrome DevTools, you can check if an element creates a stacking context by selecting it in the Elements panel, then looking at the Computed tab. Search for "z-index" -- if it shows a computed value, the element participates in z-index stacking. In Firefox DevTools, you can enable "Show stacking contexts" in the Inspector settings, which highlights stacking context roots directly in the DOM tree. This makes it much easier to visualize the stacking hierarchy.

Practical Examples: Real-World Stacking Patterns

Now let us apply everything we have learned to build common UI patterns that rely on correct z-index management.

Modal / Dialog Pattern

Complete Modal with Proper Stacking

<!-- HTML: Modal at the top level of body to avoid stacking issues -->
<body>
    <header class="site-header">
        <nav>Navigation</nav>
    </header>
    <main class="site-content">
        <!-- Page content with various stacking contexts -->
        <div class="card-with-transform">
            <!-- This creates a stacking context due to transform -->
        </div>
    </main>

    <!-- Modal is a SIBLING of header and main, not nested inside them -->
    <div class="modal-overlay" id="modal">
        <div class="modal-dialog">
            <div class="modal-header">
                <h2>Modal Title</h2>
                <button class="modal-close">&times;</button>
            </div>
            <div class="modal-body">
                <p>Modal content goes here.</p>
            </div>
        </div>
    </div>
</body>

/* CSS */
:root {
    --z-header:  20;
    --z-overlay: 40;
    --z-modal:   50;
}

.site-header {
    position: sticky;
    top: 0;
    z-index: var(--z-header);
    background: white;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    z-index: var(--z-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Hidden by default */
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0.3s ease;
}

.modal-overlay.active {
    opacity: 1;
    visibility: visible;
}

.modal-dialog {
    background: white;
    border-radius: 12px;
    padding: 24px;
    max-width: 500px;
    width: 90%;
    z-index: var(--z-modal);
    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
    transform: translateY(-20px);
    transition: transform 0.3s ease;
}

.modal-overlay.active .modal-dialog {
    transform: translateY(0);
}

.modal-close {
    position: absolute;
    top: 12px;
    right: 16px;
    background: none;
    border: none;
    font-size: 24px;
    cursor: pointer;
    color: #666;
}

Dropdown Menu Pattern

Dropdown Menu with Correct Stacking

<nav class="navigation">
    <div class="nav-item has-dropdown">
        <button class="nav-link">Products</button>
        <div class="dropdown-menu">
            <a href="#" class="dropdown-item">Product A</a>
            <a href="#" class="dropdown-item">Product B</a>
            <a href="#" class="dropdown-item">Product C</a>
        </div>
    </div>
    <div class="nav-item">
        <a class="nav-link" href="#">About</a>
    </div>
</nav>

/* CSS */
.navigation {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    background: white;
    display: flex;
    gap: 4px;
    padding: 0 16px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.nav-item {
    position: relative; /* Positioning context for dropdown */
}

.dropdown-menu {
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 200px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
    padding: 8px 0;
    /* z-index not needed here because the nav itself
       is positioned and has a z-index. The dropdown
       inherits the nav's stacking context position. */
    /* But if the dropdown needs to be above sibling nav items: */
    z-index: var(--z-dropdown);
    /* Hidden by default */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: all 0.2s ease;
}

.nav-item:hover .dropdown-menu,
.nav-item:focus-within .dropdown-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.dropdown-item {
    display: block;
    padding: 10px 16px;
    color: #333;
    text-decoration: none;
    transition: background-color 0.15s ease;
}

.dropdown-item:hover {
    background: #f5f5f5;
}

Tooltip Pattern

Tooltip with Stacking Context Awareness

/* Tooltip container */
.tooltip-wrapper {
    position: relative;
    display: inline-block;
}

/* Tooltip content */
.tooltip-content {
    position: absolute;
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    background: #333;
    color: white;
    padding: 8px 14px;
    border-radius: 6px;
    font-size: 13px;
    white-space: nowrap;
    z-index: var(--z-tooltip);
    /* Hidden by default */
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease, visibility 0.2s ease;
    pointer-events: none; /* Prevent tooltip from blocking clicks */
}

/* Tooltip arrow */
.tooltip-content::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-top-color: #333;
}

/* Show on hover and focus */
.tooltip-wrapper:hover .tooltip-content,
.tooltip-wrapper:focus-within .tooltip-content {
    opacity: 1;
    visibility: visible;
}

/* IMPORTANT: If the tooltip is inside a container with
   overflow: hidden, the tooltip will be clipped.
   Solution: Use a "portal" approach -- render the tooltip
   at the body level and position it with JavaScript */

/* Alternative: Ensure the parent does NOT clip */
.tooltip-safe-parent {
    overflow: visible; /* Allow tooltip to escape */
    isolation: isolate; /* Contain z-index but allow overflow */
}

/* Tooltip variations */
.tooltip-content.tooltip-right {
    bottom: auto;
    left: calc(100% + 8px);
    top: 50%;
    transform: translateY(-50%);
}

.tooltip-content.tooltip-right::after {
    top: 50%;
    left: auto;
    right: 100%;
    transform: translateY(-50%);
    border: 6px solid transparent;
    border-right-color: #333;
}

Sticky Header with Dropdown and Toast Notifications

Complete Stacking System

/* Z-index scale */
:root {
    --z-below:     -1;
    --z-normal:     0;
    --z-dropdown:  10;
    --z-sticky:    20;
    --z-fixed:     30;
    --z-overlay:   40;
    --z-modal:     50;
    --z-popover:   60;
    --z-tooltip:   70;
    --z-toast:     80;
}

/* Sticky header -- stays above scrolling content */
.site-header {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    background: white;
}

/* Content cards that might have transforms */
.content-card {
    position: relative;
    isolation: isolate; /* Contain all z-index within the card */
    /* This prevents card hover effects (transform, shadow animations)
       from creating stacking context issues */
}

.content-card:hover {
    transform: translateY(-4px);
    /* Because of isolation: isolate on the card,
       this transform does not affect external stacking */
}

/* Toast notifications -- always on top */
.toast-container {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none; /* Allow clicking through empty space */
}

.toast {
    background: #333;
    color: white;
    padding: 14px 20px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
    pointer-events: auto; /* Re-enable clicks on the toast itself */
    animation: toast-in 0.3s ease-out;
}

@keyframes toast-in {
    from {
        opacity: 0;
        transform: translateX(100px);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}
Exercise 1: Create a page layout with a sticky header (z-index for sticky), a main content area with overlapping cards (using transform on hover), and a dropdown menu in the header. The dropdown must appear above the content cards even when the cards have transforms applied. Use CSS custom properties for all z-index values and use isolation: isolate on the content cards to prevent their transforms from interfering with the header's dropdown.
Exercise 2: Build a stacking context demo that visually demonstrates the "trapped z-index" problem. Create two sibling containers, each with a positioned child. Give the first child z-index: 9999 and the second child z-index: 1. Then give the parent containers z-index values of 1 and 2 respectively, showing that the child with z-index 9999 still appears behind the child with z-index 1 because its parent has a lower stacking context. Add annotations explaining why this happens.
Exercise 3: Create a complete notification system with four layers: (1) a sticky navigation bar, (2) a modal overlay with a modal dialog, (3) tooltips that appear when hovering elements inside the modal, and (4) toast notifications that appear above everything including the modal. Define a z-index scale using CSS custom properties with gaps between values. Ensure each component uses the appropriate z-index from the scale. Test that tooltips inside the modal appear above the modal content but below the toast notifications.