Build a Controlled React-Bootstrap Modal That Closes Reliably
Learn how to implement a controlled Modal in React-Bootstrap by synchronizing the show prop with onHide, using portals, and handling scrollable content.
10 Feb 2026, 06:32 UTC

The Problem: A Modal That Won’t Take “No” for an Answer
The most common React-Bootstrap Modal failure is not a styling bug; it is a state-synchronization bug. You click the backdrop or press Escape, the overlay visually disappears, but the component stays mounted—or worse, it never closes at all. The fix is to treat the Modal as a fully controlled component: your parent state decides when it renders, and the Modal asks your state to change through onHide.
Desired Outcome
A dialog that opens from a user action, traps keyboard focus, renders outside your application root, and closes deterministically from the header close button, a backdrop click, or the Escape key. When closed, the modal subtree should be removed from the DOM rather than merely hidden with CSS.
Prerequisites
- A React application using function components and hooks.
react-bootstrapand the Bootstrap stylesheet installed in your project.
Run the following in your project root (normal package-install permissions are sufficient):
npm install react-bootstrap bootstrap
Import the Bootstrap CSS once in your application entry point, for example src/index.js or src/main.jsx:
import 'bootstrap/dist/css/bootstrap.min.css';
Expected check: the install completes without peer-dependency errors, and the CSS import resolves. Risk: omitting the stylesheet leaves the modal unstyled and may make the backdrop invisible.
Focused Procedure
1. Own the visibility state
Create a boolean state variable and its setter in the component that renders the trigger and the modal. This variable is the single source of truth for whether the dialog is open.
import { useState } from 'react';
import { Button, Modal } from 'react-bootstrap';
function ConfirmDialog() {
const [show, setShow] = useState(false);
return (
<>
<Button variant="primary" onClick={() => setShow(true)}>
Open dialog
</Button>
<Modal show={show} onHide={() => setShow(false)}>
<Modal.Header closeButton>
<Modal.Title>Confirm action</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>This action cannot be undone.</p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setShow(false)}>
Cancel
</Button>
<Button variant="primary" onClick={() => setShow(false)}>
Confirm
</Button>
</Modal.Footer>
</Modal>
</>
);
}
The show prop is the input; onHide is the output. When the user clicks the backdrop, the header close button, or presses Escape, React-Bootstrap calls onHide. If that callback does not update the state bound to show, the modal has no way to close.
2. Use the semantic sub-components
Modal.Header, Modal.Body, and Modal.Footer are not required, but they provide the expected Bootstrap layout classes and ARIA landmarks. The closeButton prop on Modal.Header renders an accessible dismiss control that triggers onHide automatically.
3. Constrain long content
If Modal.Body contains enough data to exceed the viewport height, the dialog can grow taller than the screen. Add the scrollable prop to the Modal so the body scrolls independently while the header and footer remain visible.
<Modal show={show} onHide={() => setShow(false)} scrollable>
{/* ... */}
</Modal>
Expected Checks
- Close via built-in triggers: Click the backdrop, the header close button, and press
Escape. Each should callonHide, update state, and remove the modal subtree from the DOM. - Portal placement: Open your browser’s element inspector. The modal markup should appear at the end of the
<body>element, not inside your React root container. React-Bootstrap uses the React Portal API to avoid CSSz-indexandoverflowclipping issues in ancestor containers. - Keyboard focus: With the modal open, press
Tabrepeatedly. Focus should cycle through the modal’s interactive elements and not escape to the page behind it. PressEscapeto close. - Scrollable behavior: If you enabled
scrollable, resize the browser window so the modal is taller than the viewport. TheModal.Bodyregion should scroll while the header and footer stay fixed.
Recovery Options
| Symptom | Likely cause | Recovery |
|---|---|---|
Modal does not close on backdrop or Escape |
onHide is missing or does not update the state bound to show |
Pass onHide={() => setShow(false)} and ensure the same state variable controls show |
| Modal content is cut off at the bottom of the screen | Body content exceeds viewport height without scrolling | Add the scrollable prop to Modal |
| Nested modals stack incorrectly or appear behind page content | Custom CSS overrides Bootstrap overlay classes such as .modal or .modal-backdrop |
Remove hardcoded z-index rules and let React-Bootstrap manage stacking |
| Focus leaks to background page while modal is open | Custom focus management conflicts with built-in focus trapping | Remove manual autoFocus or focus-stealing logic inside the modal subtree |
Limitations and Verification Notes
This guide describes behavior common to recent React-Bootstrap major releases. If your project uses an older version, class names and portal behavior may differ. Because library APIs evolve, verify prop names such as scrollable and onHide against the documentation for your installed version before relying on them in production.
Custom CSS that overrides Bootstrap’s overlay classes is the most frequent source of stacking and clipping regressions. If you must customize appearance, prefer additive classes over modifying the base .modal styles.
To confirm the implementation works in your environment, open the modal, trigger each close path, and inspect the DOM to ensure the portal node is removed when show is false. If any path leaves the modal mounted, trace the state update back to the onHide handler.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.