Opacity break z-index bugs happen when an element with opacity below 1 creates a stacking context that traps children inside a local layer.
CSS Stacking Context Fix
Why Does opacity Break Z-index?
Opacity break z-index issues usually happen because opacity values below 1 create a new stacking context. That means a tooltip, dropdown, modal, badge, or toast inside a faded parent can get trapped below elements outside that parent, even when the child has a large z-index.
This bug is sneaky because opacity feels like a visual property only. You lower opacity to fade a card, disable a menu, dim a section, or animate a component. Then a child popup suddenly refuses to appear above a header, overlay, sticky bar, or nearby card. The child z-index is not necessarily wrong. It may simply be competing inside the wrong local stacking context.
- opacity
- z-index
- stacking context
- overlays
Test the faded parent first
Temporarily change parent opacity back to 1 in DevTools. If the tooltip, modal, dropdown, or toast jumps to the correct layer, the problem is an opacity stacking context.
Related: Try this in the FrontFixer Live Inspector.
Open Live Inspector→What the bug looks like
A tooltip, menu, modal, or badge stays behind another element even with a large z-index.
Why it happens
A parent with opacity below 1 becomes a new stacking context and traps children.
What usually fixes it
Do not fade the parent that owns overlays. Fade an inner visual layer or use a separate overlay root.
Why opacity changes the stacking rules
opacity looks harmless because it does not move the element, change its size, or visibly alter the layout flow. But when opacity is less than 1, the browser has to paint that element and its children together as a group. That group becomes a stacking context.
Once that happens, the children inside the faded parent do not compete directly with the rest of the page. They compete inside their parent group. Then the entire faded group competes against other page layers. A child with z-index:9999 can still lose if the faded parent group is underneath another stacking context.
The clean solution is not to keep raising z-index. The clean solution is to decide what should be faded and what should remain free. Fade the card surface, background, or disabled visual state. Do not trap important overlays inside the faded parent.
A tooltip is inside a faded card
The most common version happens when a card uses opacity for a disabled, loading, hover, or inactive state. A tooltip or badge inside the card receives a huge z-index, but it still cannot rise above elements outside the faded card.
Broken code
Tooltip inside opacity parent.card.is-muted {
opacity: .75;
}
.card .tooltip {
position: absolute;
z-index: 9999;
}
Broken visual result
Correct code
Fade only inner surface.card__surface.is-muted {
opacity: .75;
}
.tooltip {
position: absolute;
z-index: 40;
}
Fixed visual result
A disabled menu fades the dropdown owner
A disabled or inactive navigation group may use opacity to look muted. If the dropdown still exists inside that faded group, the menu can be trapped below nearby header layers, even though the dropdown itself has a z-index.
Broken code
Dropdown inside muted group.nav-group.is-muted {
opacity: .6;
}
.nav-group .dropdown {
position: absolute;
z-index: 80;
}
Broken visual result
Correct code
Fade label, not menu owner.nav-label.is-muted {
opacity: .6;
}
.dropdown {
position: absolute;
z-index: 80;
}
Fixed visual result
A dimmed page fades the entire app shell
Some interfaces dim the whole page by applying opacity to the app shell when a modal or loading state appears. That can trap toasts, drawers, and secondary overlays inside the faded shell instead of letting them sit above the page.
Broken code
Whole shell faded.app-shell.is-dimmed {
opacity: .45;
}
.toast {
position: fixed;
z-index: 1000;
}
Broken visual result
Correct code
Use a dim overlay layer.page-dim {
position: fixed;
inset: 0;
background: rgb(15 23 42 / .55);
z-index: 40;
}
.toast-root {
position: fixed;
z-index: 60;
}
Fixed visual result
A fade animation keeps the wrong stacking context
A fade animation may temporarily set opacity below 1. During that state, the element creates a stacking context. If a menu or overlay appears while the fade state is active, it can be trapped unexpectedly.
Broken code
Fade wrapper owns overlay.panel.is-entering {
opacity: .98;
}
.panel .popover {
position: absolute;
z-index: 300;
}
Broken visual result
Correct code
Fade content, keep popover root clean.panel__content.is-entering {
opacity: .98;
}
.popover-root {
position: fixed;
z-index: 300;
}
Fixed visual result
Two production-minded opacity layer patterns
Premium opacity handling separates visual fading from overlay ownership. Below are two different production patterns: one for faded component surfaces and one for full-page dim states.
Premium code example 1
Fade surface, not overlay owner.card {
position: relative;
}
.card__surface.is-muted {
opacity: .65;
}
.card__tooltip {
position: absolute;
z-index: var(--z-tooltip);
}
Premium visual result 1
The faded surface and the tooltip owner are separate responsibilities.
Premium code example 2
Dim layer plus overlay roots:root {
--z-dim: 40;
--z-modal: 60;
--z-toast: 80;
}
.page-dim {
position: fixed;
inset: 0;
background: rgb(15 23 42 / .55);
z-index: var(--z-dim);
}
.modal-root { z-index: var(--z-modal); }
.toast-root { z-index: var(--z-toast); }
Premium visual result 2
The page is dimmed by a separate layer while modal and toast roots stay above it.
Fast practical rule
If opacity breaks z-index, stop increasing the child’s z-index and inspect the parent. Any opacity below 1 can create a local stacking context. Fade the visual surface, not the parent that owns the floating UI.
Debug checklist
- Inspect the element that refuses to appear above the page.
- Check every parent for
opacitybelow1. - Look for fade animations, loading states, disabled states, and muted wrappers.
- Temporarily set parent opacity to
1in DevTools. - Move tooltips, modals, toasts, and dropdowns outside faded parents.
- Fade an inner surface or pseudo-element instead of the overlay owner.
- Use a separate dim overlay instead of applying opacity to the app shell.
- Define named z-index tokens for dim, modal, tooltip, and toast layers.
1 and see whether the layer problem disappears..98 during an animation can still create the problem.When opacity is still the right choice
opacity is not wrong. It is useful for disabled states, transitions, skeleton loading, fades, dimmed cards, and subtle UI emphasis. The mistake is using opacity on a parent that also owns floating UI that needs to escape.
Use opacity on the smallest visual layer possible. A card surface can fade. A label can fade. A disabled content area can fade. But the tooltip, modal, dropdown, or toast root should stay in a clean stacking context.
The authority move is to separate visual fading from layer ownership.
Why this bug survives review
This bug survives because opacity changes are often state-based. The layout may work in the normal state and break only during a disabled, loading, muted, hover, or transition state. That makes the bug feel random.
A serious review tests overlays while parent components are loading, disabled, faded, animated, and dimmed. If an overlay must escape the component, it should not live inside the component’s faded layer.
Final takeaway
opacity breaks z-index when a parent with opacity below 1 creates a stacking context. The child can have a huge z-index and still lose because it is trapped inside that faded parent group.
Use opacity on inner visual surfaces, use separate dim layers for full-page states, and move overlays to clean roots when they need to escape. That keeps fades beautiful without turning z-index into a guessing game.