What is a Bootstrap modal?
Bootstrap’s modal puts content on a layer above the page, dims everything behind it with a backdrop and stops the body from scrolling. Dismiss handling is built in, so a click outside or the Escape key closes it unless you switch that off.
It ships with the Bootstrap framework and opens through data attributes or a JavaScript call, which means no custom overlay code. The official documentation mentions lightboxes and user notifications as typical uses, and says custom content works too.
In general UI work, a modal in UI design blocks the page until the user responds. Bootstrap’s version uses position: fixed, so it sits above everything else in the document. The modal content scrolls instead of the page. A click on the backdrop closes it, unless the backdrop option is set to static.
As of October 2026 the Bootstrap docs list v5.3.8. The August 25, 2025 release post said the plan was for it to be the last patch release before v5.4.0. Bootstrap 6 Alpha 1 followed on October 8, 2026, but it is a pre-release. The maintainers say Bootstrap 5 remains supported and that v5.4.0 will ship before they announce an end-of-life date for it. Everything below describes v5.3.8.
How to create a Bootstrap modal
A working modal needs the Bootstrap CSS and bundle JavaScript loaded on the page, a trigger button, and a div with the modal class that holds the dialog markup. The setup goes like this.
- Load the Bootstrap CSS in the head and bootstrap.bundle.min.js before the closing body tag, from jsDelivr or after running npm i bootstrap.
- Add a button with data-bs-toggle=”modal” and data-bs-target=”#exampleModal”.
- Add a div with the classes modal and fade, the id exampleModal and tabindex=”-1″, placed at the top level of the body.
- Nest modal-dialog and modal-content inside it, then the header, body and footer sections.
- Add a close button carrying data-bs-dismiss=”modal” to the header or footer.
Required markup structure
The outer container carries the modal class, an id, tabindex=”-1″ and, if you want the animation, fade. Inside it, modal-dialog sets the width and margin, and modal-content is the visible box with the border and background. The header, body and footer live in there. Only modal-body is required (it supplies the padding), and the footer is optional.
The docs ask for a header with a dismiss action, or another explicit dismiss control, whenever possible. They also suggest making modal-title an h1 element sized down with the fs-5 utility, since a dialog is its own document context.
Without Bootstrap’s JavaScript nothing toggles the markup. According to the official JavaScript documentation, the plugins have no fallback when JavaScript is unavailable.
Bootstrap 4 versus Bootstrap 5 attributes
| Item | Bootstrap 4 | Bootstrap 5 |
|---|
| Open trigger | data-toggle, data-target | data-bs-toggle, data-bs-target |
| Dismiss attribute | data-dismiss | data-bs-dismiss |
| Close button class | close | btn-close |
| jQuery | Required | Not required (dropped in 5.0.0) |
The migration guide says every plugin’s data attributes are now namespaced with bs. The plugin only listens for the namespaced names, so a Bootstrap 4 snippet pasted into a Bootstrap 5 page leaves the button dead.
How to open and close a Bootstrap modal
Put data-bs-toggle=”modal” and a data-bs-target on a button, or call show() on a modal instance, and the modal opens. Closing works through data-bs-dismiss=”modal” or hide().
Opening with data attributes
Bootstrap’s own docs prefer this method, and it needs no script. Put data-bs-toggle=”modal” on the controller element, then point data-bs-target=”#id” (or href=”#id”) at the modal.
There’s one catch. A single element takes only one set of data attributes, so one button can’t trigger a tooltip and a modal at the same time (Bootstrap JavaScript docs). Put the tooltip on a wrapper element instead.
Opening with JavaScript
The JavaScript route starts with a constructor that accepts an element or a CSS selector. It sits in Bootstrap’s programmatic API next to the static lookup methods.
- new bootstrap.Modal(‘#myModal’, options) creates an instance.
- getOrCreateInstance returns the existing instance or builds one, so repeated clicks reuse a single object.
- getInstance returns null when the element was never initialized.
- show(), hide() and toggle() open, close and flip the modal.
To open a modal on page load, create the instance and call show() once the DOM is ready.
None of this needs jQuery in Bootstrap 5, though when jQuery is present on the page Bootstrap also registers its plugins on it (official docs).
Closing a modal
Where you put the close control matters, and only one placement is recommended.
Inside the modal, a button with data-bs-dismiss=”modal” does the job. Most setups have the header close button plus a Close button in the footer.
Outside the modal, the same attribute works if you add a data-bs-target pointing at the modal id. The docs warn that this does not match the ARIA Authoring Practices Guide dialog pattern and say to use it at your own risk.
A click on the backdrop and the Escape key also close a modal by default. The backdrop and keyboard options switch both behaviors off.
Bootstrap modal sizes and layout classes
Width comes from a size class on the modal-dialog element. These sizes only apply from certain breakpoints up, which prevents horizontal scrollbars on narrow viewports (Bootstrap docs, v5.3).
The figures from the v5.3 docs:
- modal-sm caps the width at 300px.
- With no class you get the default 500px.
- modal-lg gives 800px.
- modal-xl goes up to 1140px.
- The fullscreen-below-a-breakpoint classes, modal-fullscreen-sm-down to modal-fullscreen-xxl-down, cover breakpoints from 576px to 1400px.
Fullscreen and responsive fullscreen
modal-fullscreen covers the whole viewport at every width.
The -down variants apply only below a breakpoint. modal-fullscreen-md-down goes fullscreen under 768px and stays a regular dialog above it, which is plain responsive design applied to a dialog.
For forms I’d make modal-fullscreen-md-down the default, since phones then get the full screen for inputs.
Centered and scrollable modals
modal-dialog-centered centers the dialog vertically. modal-dialog-scrollable keeps the header and footer in place and scrolls only the modal body once content outgrows the viewport. Leave it off and the whole modal scrolls over the page instead. Add both classes for a centered modal with a scrolling body.
Using the grid inside a modal
The docs give one rule for layouts inside a modal. Nest a container-fluid inside modal-body, then use the normal grid system classes.
Grid breakpoints follow the viewport width, not the modal width. A col-md-4 column inside a 500px modal still switches layout at a 768px viewport, which surprises people the first time.
Which options, methods and events control a Bootstrap modal?
The options are few. Bootstrap exposes seven methods in all and the modal fires five events, so most of the scripting weight sits in those last two.
Options and defaults
The three options are backdrop, keyboard and focus.
| Option | Accepts | Default | Effect |
|---|
| backdrop | true, false, “static” | true | Adds a backdrop. Static blocks closing on outside click |
| keyboard | boolean | true | Escape closes the modal |
| focus | boolean | true | Moves focus to the modal on initialization |
Options pass through data attributes in kebab-case (data-bs-backdrop=”static”) or through a JavaScript object. When both are set, the JavaScript object wins.
Since Bootstrap 5.2.0 an experimental data-bs-config attribute accepts the same settings as JSON. Defaults change globally with bootstrap.Modal.Default.keyboard = false (official JavaScript docs).
Static backdrop and preventing close
Set backdrop to static and keyboard to false, and the modal ignores outside clicks and the Escape key. In markup that means data-bs-backdrop=”static” and data-bs-keyboard=”false” on the modal div.
Each blocked click or Escape press fires hidePrevented.bs.modal. From script, hide.bs.modal can be cancelled with preventDefault(), which blocks any close, including one triggered by hide().
Static suits forms with unsaved input. I’d skip it for plain information, where users expect a click outside to dismiss the modal.
Methods and the async caveat
Every modal method is asynchronous. It returns as soon as the transition starts, before shown.bs.modal or hidden.bs.modal fires (Bootstrap docs).
A call made while the modal is mid-transition is ignored. Bootstrap’s Sass default for the modal transition is transform .3s ease-out (v5.3.8), short enough to miss in testing, but a call inside it is dropped.
Beyond the ones covered above, handleUpdate() readjusts the position when the modal height changes while it’s open, for example when a scrollbar appears. dispose() removes the stored data on the element. Calling dispose() right after hide() gives incorrect results, so wait for hidden.bs.modal and then dispose.
Events and firing order
A normal open and close fires show, shown, hide and hidden, in that order. All modal events fire on the modal element itself.
- show.bs.modal fires immediately when show is called, before the transition.
- shown.bs.modal waits until the modal is visible and the transition has finished.
- hide.bs.modal fires immediately on hide and can be cancelled.
- hidden.bs.modal comes after the modal is fully hidden.
hidePrevented.bs.modal sits outside that sequence. It fires when a static backdrop or keyboard: false blocks a close.
Loading different content into one modal
One modal can serve many buttons. Give each button a data-bs-* attribute (the docs use data-bs-whatever), read event.relatedTarget inside a show.bs.modal listener, and write the value into the modal title or a form field.
Because show.bs.modal fires before the transition, the swap happens out of sight. For remote data, start an Ajax request in the same listener and fill the modal when the response arrives.
relatedTarget is set when a click opens the modal. Calling show(trigger) from JavaScript passes the same element along.
How to style and animate a Bootstrap modal
Override the CSS variables set on .modal and .modal-backdrop, or edit the Sass variables before compiling. Animation runs through the fade class and two transform variables.
The CSS variables, added in v5.2.0, are scoped to the component, so one modal can differ from the rest.
- –bs-modal-width, –bs-modal-bg, –bs-modal-color
- –bs-modal-border-radius and –bs-modal-box-shadow
- –bs-modal-header-padding-x and –bs-modal-header-padding-y
- –bs-backdrop-bg and –bs-backdrop-opacity, which live on the backdrop
The values come from Sass, so a Sass build keeps working. A project-wide change belongs in Sass, and a one-off change goes on the element.
The backdrop defaults to black at an opacity of .5 ($modal-backdrop-bg and $modal-backdrop-opacity). Raising –bs-backdrop-opacity on a single modal is the quickest way to separate it from busy content.
The shadow is one variable per modal, and a CSS box shadow generator produces the value to paste in.
On the animation side, $modal-fade-transform sets the dialog’s start state and $modal-show-transform sets the end state. The default start is translate(0, -50px), a slide down from above, and $modal-fade-transform: scale(.8) turns it into a zoom-in. Remove the fade class and the modal appears with no transition at all.
Per the Bootstrap accessibility docs, most transition effects, including modal open and close, switch off for users who set prefers-reduced-motion. Animations added through your own keyframes sit outside that rule, so wrap them in a media query for the same preference.
Bootstrap 5.3.0 added color modes. Set data-bs-theme=”dark” on the modal element and the dialog follows the dark palette. The btn-close-white class is deprecated for the same job, so put the attribute on the close button or a parent instead (Bootstrap migration guide).
How to make a Bootstrap modal accessible
Bootstrap adds role=”dialog” in JavaScript, so you never write it by hand. What’s left is naming the dialog with aria-labelledby, pointing at its title, and placing focus deliberately when the modal opens.
The ARIA attributes that name and describe the dialog are in the table.
| Attribute | Goes on | Job |
|---|
| aria-labelledby | Modal container | Points to the visible title |
| aria-describedby | Modal container | Optional short description |
| aria-label=”Close” | Close button | Names the icon-only button |
The W3C ARIA Authoring Practices Guide adds one rule. Skip aria-describedby when the content holds lists, tables or several paragraphs, because a screen reader reads that description as one unbroken string.
On focus, the W3C dialog pattern says the first focusable element is right for most dialogs. When the content is long or structured, a static element with tabindex=”-1″ at the top, such as the title, works better. And if the dialog confirms a delete or a payment, focus goes to the least destructive button.
The HTML autofocus attribute does nothing inside a Bootstrap modal (official docs). Focus the chosen element from a shown.bs.modal listener instead, which for a delete confirmation means the Cancel button.
The same W3C pattern requires Tab and Shift+Tab to cycle inside the dialog and focus to return to the invoking element on close. Bootstrap’s docs say its components are designed to work for keyboard and mouse users as well as touch, but overall web accessibility still comes down to the author’s markup and scripts. Test both behaviors by hand.
One reported failure is GitHub issue twbs/bootstrap #39862. It describes a full Tab cycle inside an open modal scrolling the page to wherever the modal markup sits in the DOM, including on Bootstrap’s own 5.3 demo. Maintainers confirmed it in July 2024. The issue was closed on June 23, 2026, with the explanation that the cause was Bootstrap’s scripted focus trap and that the v6 development branch relies on the native dialog element’s focus handling instead. The issue page links no fix for v5, so test the Tab cycle with the markup in its final position.
When does a Bootstrap modal stop working?
The documented trouble spots are a second modal opening on top of the first, markup sitting inside another fixed element, a Bootstrap version that differs from the one the code was written for, and mobile browsers scrolling the page behind the dialog.
Nested modals
Bootstrap supports one modal window at a time. The maintainers consider nested modals a poor user experience, so they’re unsupported.
What is supported is a swap. Put data-bs-toggle=”modal” and data-bs-target=”#secondModal” on a button inside the first modal. The first modal closes as the second opens, and a Back button carrying the first modal’s id swaps them again. Two modals are never open together.
Backdrop and z-index problems
- The page is dimmed and the modal won’t respond to clicks. That usually means the modal sits inside another fixed element, which the docs flag as a source of rendering problems.
- A navbar or custom element stays above the backdrop when its z-index is higher than the modal scale.
- A toast showing over an open modal is expected, since toasts sit at 1090 and the modal at 1055 in v5.3.8.
Move the modal markup out of the fixed container before touching any z-index value.
Version differences
| Bootstrap docs version | Backdrop z-index | Modal z-index |
|---|
| 4.6 | 1040 | 1050 |
| 5.0.2 | 1040 | 1060 |
| 5.3.8 | 1050 | 1055 |
Code pinned to one value breaks on another. The ng-bootstrap issue #4139 reports a stacked-modal backdrop fixed at 1050 that stopped working when Bootstrap 5 moved the modal to 1060.
Bootstrap’s docs say that changing one value in the scale likely means changing all of them. If an override is unavoidable, follow a clean approach to overriding Bootstrap CSS and adjust the whole scale.
Internet Explorer is a dead end. The Bootstrap 5 docs say IE is not supported and send IE users to Bootstrap v4, but Bootstrap 4 reached end of life on January 1, 2023 (Bootstrap v4.6.2 release post). A project that needs IE has no supported modal to use.
Mobile scrolling quirks
Support for overflow: hidden on the body is limited on iOS and Android. Scrolling past the top or bottom of a modal can scroll the page underneath, and a gesture that starts inside an input or textarea scrolls the body instead of the modal (Bootstrap browsers and devices docs).
The docs still carry both notes at v5.3.8, and the iOS entry is dated to iOS 9.2. Run a cross-browser compatibility check on current devices before designing around either one.
Bootstrap modal vs offcanvas, toast and the native dialog element
Reach for the Bootstrap modal when the user has to answer before continuing. Side panels belong in an offcanvas, and notices that need no reply belong in a toast. If you want a modal without Bootstrap JavaScript, the native dialog element does that.
| Component | Blocks the page | Best use |
|---|
| Bootstrap modal | Yes, backdrop and body scroll removed | Confirmations, short forms |
| Offcanvas | Yes by default, backdrop and scroll are options | Navigation, carts |
| Toast | No | Brief notices |
| Native dialog (showModal) | Yes, rest of page inert | Modal with no framework |
An offcanvas is a sidebar that slides in from any viewport edge, the same idea as a drawer in UI design. It shares code with the modal, and only one can show at a time.
Its defaults are 400px wide for left and right placement, 30vh tall for top and bottom, and body scrolling off while it is open (Bootstrap docs, v5.3).
A toast notification blocks nothing, hides itself after 5000 ms by default and must be initialized in script.
I’d pick a toast over a modal for confirmations that need no decision, since modals interrupt and toasts don’t. Controls inside an autohiding toast are hard for keyboard users to reach in time, so anything actionable belongs in a modal or an alert.
On the native side, showModal() opens a modal dialog, makes everything outside it inert, closes on Escape and styles its backdrop through ::backdrop. MDN lists it as Baseline widely available, supported across browsers since March 2022.
It ships no size classes, grid or Sass variables, and animating it takes @starting-style plus allow-discrete transitions (MDN).
On a page that already loads Bootstrap, the Bootstrap modal is the shorter path, because sizes, scrollable bodies and CSS variables come ready. On a page without Bootstrap, the native element saves you from shipping a framework for one dialog.
Bootstrap Modal FAQ
How do you add a form to a modal?
Put the form inside modal-body and give it an id. The footer sits outside that element, so a footer button needs form=”yourFormId” with type=”submit” to submit it.
Bootstrap’s own varying-content demo uses this layout, with the fields in the body and the buttons in the footer.
Why does a YouTube video keep playing after the modal closes?
Bootstrap does not control embedded players, and its docs say stopping playback takes extra JavaScript.
Listen for hidden.bs.modal, then reset the iframe src or call the player’s pause method.
Do tooltips and popovers work inside a modal?
Yes. The docs allow both inside modals and dismiss them automatically when the modal closes.
They still depend on Popper, which bootstrap.bundle.min.js already includes. With bootstrap.min.js, load Popper separately.
Can you use the modal JavaScript in React, Vue or Angular?
Not reliably. Bootstrap’s docs say its JavaScript is not fully compatible with those frameworks, because both can end up mutating the same DOM element and cause bugs such as dropdowns stuck open.
A framework-specific package is the better route. That means React Bootstrap for React, ng-bootstrap or ngx-bootstrap for Angular, and BootstrapVueNext for Vue 3 (alpha in the v5.3.8 docs).
Checks to Run Before a Bootstrap Modal Ships
Before release, a modal needs four checks: the Bootstrap version in production, the markup’s final DOM position, a keyboard-only pass, and a test on a real phone. The order matters.
- Version goes first, since a wrong attribute prefix blocks opening.
- DOM position comes next, because stacking can hide the dialog.
- The keyboard and phone passes go last, as both need an open modal.
Each check takes minutes, and only the phone pass needs hardware. Folding them into a full web accessibility checklist keeps the keyboard pass from getting skipped under deadline.
These checks were verified against the Bootstrap docs at v5.3.8 on October 28, 2026, and any minor or major upgrade reopens the first one.