---
name: bootstrap5-modal-backdrop-stacking
description: |
  Fix Bootstrap 5 modals trapped behind their backdrop (grayed out, unclickable) when modals
  are nested inside positioned containers like #wrapper or #page-wrapper. Use when:
  (1) modal appears but is unclickable with gray overlay on top,
  (2) `.modal-backdrop` has higher effective z-index than `.modal`,
  (3) page uses Bootstrap 3-to-5 migration with legacy wrapper divs,
  (4) modal is inside a parent with position:relative/absolute + z-index, transform, opacity,
  filter, or other stacking-context-creating CSS. Applies to Bootstrap 5.x modals, SweetAlert2
  interactions, and any project using `data-bs-toggle="modal"` or `bootstrap.Modal`.
author: Claude Code
version: 1.0.0
date: 2026-02-12
---

# Bootstrap 5 Modal Backdrop Stacking Context Fix

## Problem

Bootstrap 5 modals appear behind their backdrop overlay, making the page grayed out and
completely unclickable. The modal content is invisible or unreachable. This is especially
common in codebases migrated from Bootstrap 3 to Bootstrap 5 that retained legacy wrapper
div structures.

## Context / Trigger Conditions

You should apply this fix when ANY of these symptoms appear:

- **Modal opens but screen is gray and unclickable** — the `.modal-backdrop` sits above the `.modal`
- **`z-index` on `.modal` (1055) appears correct** but is ineffective due to stacking context
- **Modals are nested inside containers** like `#wrapper`, `#page-wrapper`, `<div class="content-wrapper">`,
  or any element with CSS that creates a new stacking context
- **Bootstrap 3 to 5 migration** — old markup placed modals inside layout wrappers that now
  have positioning/transform CSS
- **Multiple modal backdrop orphans** — closing one modal leaves `.modal-backdrop` elements stuck in DOM

### CSS Properties That Create Stacking Contexts (Root Cause)

A parent element with ANY of these traps child z-index values:

- `position: relative/absolute/fixed/sticky` + `z-index` (any value, even `z-index: 0`)
- `transform` (any value, even `transform: translateZ(0)`)
- `opacity` < 1
- `filter` (any value)
- `perspective` (any value)
- `mix-blend-mode` (anything other than `normal`)
- `isolation: isolate`
- `will-change` (specifying any of the above properties)
- `contain: layout` or `contain: paint`

## Solution

### Fix 1: Move Modals to `<body>` via JavaScript (Recommended)

Add this at the start of your page's script initialization. This is the safest fix because
it doesn't require changing HTML structure or CSS:

```javascript
// Move modals to <body> to escape stacking context traps
['myModalId', 'anotherModalId'].forEach(id => {
    const el = document.getElementById(id);
    if (el) document.body.appendChild(el);
});
```

For dynamically created modals (e.g., from AJAX), move them after creation:

```javascript
function showDynamicModal(html) {
    const container = document.createElement('div');
    container.innerHTML = html;
    const modal = container.firstElementChild;
    document.body.appendChild(modal);
    new bootstrap.Modal(modal).show();
}
```

### Fix 2: Place Modal HTML Outside Wrapper (Template Fix)

If you control the template, place modals as direct children of `<body>`, not inside
layout wrappers:

```html
<body>
    <div id="wrapper">
        <!-- Page content here -->
    </div>

    <!-- Modals OUTSIDE wrapper -->
    <div class="modal fade" id="myModal">...</div>
</body>
```

### Fix 3: Backdrop Cleanup (Complementary)

Bootstrap 5 can orphan `.modal-backdrop` elements, especially when modals are shown/hidden
programmatically or when transitioning between modals. Add cleanup handlers:

```javascript
document.querySelectorAll('.modal').forEach(modalEl => {
    modalEl.addEventListener('hidden.bs.modal', () => {
        // Only clean up if no modals are still open
        if (!document.querySelector('.modal.show')) {
            document.querySelectorAll('.modal-backdrop').forEach(b => b.remove());
            document.body.classList.remove('modal-open');
            document.body.style.overflow = '';
            document.body.style.paddingRight = '';
        }
    });
});
```

### Fix 4: Use `getOrCreateInstance` (Avoid Duplicate Instances)

When showing/hiding modals programmatically, always use `getOrCreateInstance` to prevent
creating duplicate modal instances (which cause double backdrops):

```javascript
// GOOD
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('myModal'));

// BAD - creates new instance each time, can cause duplicate backdrops
const modal = new bootstrap.Modal(document.getElementById('myModal'));
```

## Verification

1. Open browser DevTools
2. Inspect the `.modal-backdrop` element — check its computed `z-index` (should be 1050)
3. Inspect the `.modal` element — check its computed `z-index` (should be 1055)
4. If modal's effective z-index is lower despite the CSS value, it's trapped in a stacking context
5. After fix: modal should appear above backdrop and be fully interactive

### Chrome DevTools Stacking Context Debugging

Use the "CSS Stacking Context Inspector" Chrome extension to visualize stacking contexts,
or manually check parent elements for the CSS properties listed above.

## Example: BuyerKiosk Floor Plan Layouts Page

The layouts page had modals inside `#wrapper > #page-wrapper > .content-wrapper`. The
`#page-wrapper` had positioning CSS that created a stacking context, trapping the modals.

**Before (broken):**
```html
<body>
<div id="wrapper">
    <div id="page-wrapper">
        <!-- content -->
        <div class="modal fade" id="diffModal">...</div>  <!-- TRAPPED -->
    </div>
</div>
</body>
```

**After (fixed with JS move):**
```javascript
['diffModal', 'tasksModal', 'generateTasksModal'].forEach(id => {
    const el = document.getElementById(id);
    if (el) document.body.appendChild(el);
});
```

## Notes

- This is a **long-standing Bootstrap issue** documented in [twbs/bootstrap#16148](https://github.com/twbs/bootstrap/issues/16148) and [#23916](https://github.com/twbs/bootstrap/issues/23916)
- Bootstrap's official docs state: "Whenever possible, place your modal HTML in a top-level position to avoid potential interference from other elements"
- SweetAlert2 manages its own overlay system and is generally not affected, but mixing SweetAlert2 and Bootstrap modals on the same page can cause backdrop conflicts
- When transitioning between modals (hiding one, showing another), add a small delay or use the `hidden.bs.modal` event to ensure clean transitions
- The `data-bs-dismiss="modal"` attribute on close buttons works independently of JS instance management

## References

- [Bootstrap 5 Z-Index Documentation](https://getbootstrap.com/docs/5.1/layout/z-index/)
- [Bootstrap 5 Modal Documentation](https://getbootstrap.com/docs/5.0/components/modal/)
- [Bootstrap Issue #16148 - Modal behind backdrop](https://github.com/twbs/bootstrap/issues/16148)
- [Bootstrap Issue #23916 - Modal backdrop in fixed container](https://github.com/twbs/bootstrap/issues/23916)
- [Unstacking CSS Stacking Contexts - Smashing Magazine](https://www.smashingmagazine.com/2026/01/unstacking-css-stacking-contexts/)
- [4 Reasons Z-Index Isn't Working - Coder Coder](https://coder-coder.com/z-index-isnt-working/)
