Bootstrap 5 Modal: Triggers, Sizes, and Custom Animations
The modal is one of the most widely used UI patterns on the web — confirmation dialogs, image lightboxes, lead capture forms, cookie consent banners — nearly every production site uses one. Bootstrap 5 ships with a capable, accessible bootstrap dialog component right out of the box, but most developers only scratch the surface of what it can do.
In this bootstrap modal tutorial, you will go from the basic trigger pattern all the way through responsive sizing, stacked modals, custom CSS animations, and JavaScript event hooks. Every section includes working code you can drop straight into a project. If you are already building on a solid foundation like Canvas Template, most of these patterns will slot in with minimal adjustment because the template follows Bootstrap 5 conventions throughout.
Let us get into it.
Key Takeaways
Key Takeaways
- Bootstrap 5 modals are triggered via HTML data attributes or the JavaScript API — both approaches are covered with working examples.
- Five official size modifiers exist: small, default, large, extra-large, and fullscreen (with responsive breakpoint variants).
- You can replace the default fade animation with custom CSS keyframes without touching Bootstrap source files.
- The JavaScript event lifecycle (
show.bs.modal,shown.bs.modal,hide.bs.modal,hidden.bs.modal) gives you fine-grained control over modal behaviour. - Accessibility is non-negotiable: always include
aria-labelledby,aria-modal, and correct focus management. - Canvas Template pre-wires many of these patterns so you can customise rather than build from scratch.
The Anatomy of a Bootstrap 5 Modal
Before you add any custom behaviour, you need a clean understanding of the markup structure Bootstrap 5 expects. The component is split into three nested layers: the outer wrapper (.modal), a sizing/positioning container (.modal-dialog), and the visible surface (.modal-content). Inside .modal-content you optionally place a .modal-header, .modal-body, and .modal-footer.
<!-- Trigger button -->
<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
>
Open Modal
</button>
<!-- Modal markup -->
<div
class="modal fade"
id="exampleModal"
tabindex="-1"
aria-labelledby="exampleModalLabel"
aria-modal="true"
role="dialog"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal Title</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<p>Your modal content goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
A few things worth highlighting: tabindex="-1" ensures the modal element itself is programmatically focusable without being part of the natural tab order. The aria-labelledby attribute connects the dialog to its title for screen readers. If you want to dig deeper into why these attributes matter, our post on making a Bootstrap 5 website accessible to WCAG 2.1 AA covers the full rationale and testing process.
Trigger Methods: Data Attributes vs JavaScript API
Bootstrap 5 dropped jQuery entirely, so the JavaScript API is now based on vanilla ES modules. You have two primary ways to open a modal: declarative HTML triggers and the programmatic JavaScript API.
Data Attribute Triggers
The simplest approach requires zero JavaScript. Add data-bs-toggle="modal" and data-bs-target="#yourModalId" to any clickable element:
<!-- Works on buttons, anchors, or any element -->
<a
href="#"
role="button"
data-bs-toggle="modal"
data-bs-target="#contactModal"
>
Contact Us
</a>
JavaScript API Triggers
When you need conditional logic — for example, only open the modal after form validation passes — reach for the JavaScript API:
// Get the modal element
const modalEl = document.getElementById('confirmModal');
// Instantiate (or retrieve existing instance)
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
// Open programmatically
document.getElementById('validateBtn').addEventListener('click', () => {
const input = document.getElementById('userEmail').value;
if (input.includes('@')) {
modal.show();
} else {
alert('Please enter a valid email address.');
}
});
// Close programmatically
document.getElementById('confirmBtn').addEventListener('click', () => {
modal.hide();
});
You can also pass configuration options on instantiation:
const modal = new bootstrap.Modal(modalEl, {
backdrop: 'static', // click outside does NOT close
keyboard: false, // ESC key does NOT close
focus: true // auto-focus the modal on open
});
Setting backdrop: 'static' is particularly useful for confirmation dialogs where you want to force a deliberate user decision before the modal dismisses.
Bootstrap 5 Modal Sizes and Fullscreen Variants
The .modal-dialog container accepts size modifier classes that change the maximum width of the dialog. Bootstrap 5 also introduced responsive fullscreen variants that activate only below a specific breakpoint — a welcome improvement over the old all-or-nothing fullscreen approach.
| Class | Max Width | Breakpoint Behaviour |
|---|---|---|
.modal-sm |
300px | Fixed small dialog |
| (default, no class) | 500px | Default responsive dialog |
.modal-lg |
800px | Large dialog |
.modal-xl |
1140px | Extra-large dialog |
.modal-fullscreen |
100vw / 100vh | Always fullscreen |
.modal-fullscreen-sm-down |
100vw / 100vh | Fullscreen below 576px |
.modal-fullscreen-md-down |
100vw / 100vh | Fullscreen below 768px |
.modal-fullscreen-lg-down |
100vw / 100vh | Fullscreen below 992px |
.modal-fullscreen-xl-down |
100vw / 100vh | Fullscreen below 1200px |
.modal-fullscreen-xxl-down |
100vw / 100vh | Fullscreen below 1400px |
The responsive fullscreen variants are especially useful for mobile-first modal forms. For example, a checkout form that should occupy the full screen on phones but behave as a centred dialog on desktop:
<div class="modal-dialog modal-lg modal-fullscreen-md-down">
<!-- Fullscreen on phones and tablets, large dialog on desktop -->
</div>
This mirrors the pattern used throughout Canvas Template’s prebuilt page sections, where form modals collapse gracefully on narrow viewports without a single media query written by hand.
Vertical Centering and Scrollable Modals
Two additional modifier classes on .modal-dialog address common layout problems: vertically centred modals and long-content scrollable modals.
<!-- Vertically centred -->
<div class="modal-dialog modal-dialog-centered"></div>
<!-- Scrollable body (header and footer stay fixed) -->
<div class="modal-dialog modal-dialog-scrollable"></div>
<!-- Both at once -->
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"></div>
The .modal-dialog-scrollable class is a practical solution for terms-and-conditions dialogs or long product descriptions where you want the header and footer actions to remain visible while the user scrolls through the body content. The same approach applies to the accordion-style expandable content patterns described in our deep-dive on the Bootstrap 5 Accordion and Collapse component.
Custom Modal Animations with CSS Keyframes
Bootstrap’s built-in .fade class applies a simple opacity transition. That works, but you can replace or augment it with custom keyframe animations without modifying Bootstrap source files — just override the relevant classes in your own stylesheet.
Slide-in from the Top
/* custom-modal.css */
@keyframes modalSlideDown {
from {
transform: translateY(-60px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal.fade .modal-dialog {
animation: none; /* remove Bootstrap's default transform */
}
.modal.show .modal-dialog {
animation: modalSlideDown 0.35s ease forwards;
}
Scale / Zoom-in Effect
@keyframes modalZoomIn {
from {
transform: scale(0.85);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.modal.show .modal-dialog {
animation: modalZoomIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
The cubic-bezier(0.34, 1.56, 0.64, 1) value produces a subtle spring overshoot that feels more polished than a linear ease. Keep animation durations between 250ms and 400ms — anything longer starts to feel sluggish, especially for power users who open modals frequently.
Respecting User Preferences
Always wrap custom animations in a reduced motion media query. This is a WCAG 2.1 requirement as well as good practice:
@media (prefers-reduced-motion: reduce) {
.modal.show .modal-dialog {
animation: none;
}
}
If you are theming Bootstrap with SCSS variables, you can tie animation durations to your existing token system. Our guide on using SCSS variables to theme a Bootstrap 5 site walks through exactly how to centralise these values so you only ever change them in one place.
Leveraging Bootstrap Modal JavaScript Events
Bootstrap 5 fires four custom DOM events on the modal element throughout its lifecycle. Hooking into these lets you do things like lazy-load content, reset forms, send analytics events, or prevent a modal from closing until an async operation completes.
| Event | Fires When | Cancellable |
|---|---|---|
show.bs.modal |
Immediately when show() is called |
Yes — call event.preventDefault() |
shown.bs.modal |
After the modal is fully visible (transition complete) | No |
hide.bs.modal |
Immediately when hide() is called |
Yes — call event.preventDefault() |
hidden.bs.modal |
After the modal is fully hidden (transition complete) | No |
const modalEl = document.getElementById('asyncModal');
// Lazy-load modal content on show
modalEl.addEventListener('show.bs.modal', async (event) => {
const body = modalEl.querySelector('.modal-body');
body.innerHTML = '<div class="text-center"><div class="spinner-border"></div></div>';
const response = await fetch('/api/modal-content');
const html = await response.text();
body.innerHTML = html;
});
// Reset a form after the modal fully closes
modalEl.addEventListener('hidden.bs.modal', () => {
const form = modalEl.querySelector('form');
if (form) form.reset();
});
// Prevent close until user confirms
modalEl.addEventListener('hide.bs.modal', (event) => {
const isDirty = modalEl.querySelector('form')?.dataset.dirty === 'true';
if (isDirty && !confirm('You have unsaved changes. Close anyway?')) {
event.preventDefault();
}
});
The relatedTarget property on the show.bs.modal event gives you a reference to the element that triggered the modal. This is invaluable for dynamic modals where one dialog serves multiple data sources — for example, an edit modal in a data table:
modalEl.addEventListener('show.bs.modal', (event) => {
const triggerButton = event.relatedTarget;
const userId = triggerButton.dataset.userId;
const userName = triggerButton.dataset.userName;
modalEl.querySelector('.modal-title').textContent = `Edit: ${userName}`;
modalEl.querySelector('#userId').value = userId;
});
Accessibility Best Practices for Bootstrap Modals
A bootstrap dialog component that traps keyboard users or fails to announce itself to screen readers is not just a UX problem — it can create legal exposure under accessibility regulations that are increasingly enforced in 2026. Here is the minimum checklist:
- Always include
role="dialog"andaria-modal="true"on the outer.modalelement. Bootstrap adds these automatically when using standard markup, but verify they are present if you are using a custom structure. - Link the dialog to its title using
aria-labelledbypointing to the.modal-titleelement’s ID. - For dialogs without a visible title, use
aria-labeldirectly on the.modalelement with a descriptive string. - Ensure focus moves into the modal when it opens. Bootstrap does this by default via the
focus: trueoption. Do not override this behaviour carelessly. - Trap focus within the modal while it is open. Bootstrap 5 handles this natively — do not break it by manipulating
tabindexon elements inside the modal in ways that allow focus to escape to the background. - Return focus to the trigger element after the modal closes. Again, Bootstrap handles this automatically — the trigger button regains focus when the modal hides.
<!-- Accessible alert/confirmation dialog pattern -->
<div
class="modal fade"
id="deleteConfirmModal"
tabindex="-1"
role="alertdialog"
aria-modal="true"
aria-labelledby="deleteModalTitle"
aria-describedby="deleteModalDesc"
>
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header border-0">
<h5 class="modal-title" id="deleteModalTitle">Delete Item?</h5>
</div>
<div class="modal-body" id="deleteModalDesc">
This action cannot be undone.
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
Delete
</button>
</div>
</div>
</div>
</div>
Note the use of role="alertdialog" for destructive confirmation dialogs — this signals greater urgency to assistive technologies compared to the standard role="dialog".
Real-World Modal Patterns and When to Use Them
Understanding the API is one thing. Knowing which pattern to reach for in a given design scenario is what separates good implementations from great ones.
Video Lightbox Modal
<!-- Stop video playback when modal closes -->
<div class="modal fade" id="videoModal" tabindex="-1" aria-modal="true" aria-label="Product video">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content bg-black border-0">
<div class="modal-header border-0 pb-0">
<button type="button" class="btn-close btn-close-white ms-auto" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div class="ratio ratio-16x9">
<iframe
id="videoFrame"
src=""
data-src="https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1"
title="Product video"
allowfullscreen
></iframe>
</div>
</div>
</div>
</div>
</div>
<script>
const videoModal = document.getElementById('videoModal');
const videoFrame = document.getElementById('videoFrame');
videoModal.addEventListener('show.bs.modal', () => {
videoFrame.src = videoFrame.dataset.src;
});
videoModal.addEventListener('hidden.bs.modal', () => {
videoFrame.src = '';
});
</script>
Multi-Step Form Modal
For multi-step flows inside a modal, use Bootstrap’s own d-none / d-block utilities to show and hide steps, updating an ARIA live region to announce progress to screen readers:
<div class="modal-body">
<div aria-live="polite" class="visually-hidden" id="stepAnnouncer"></div>
<div id="step1">
<!-- Step 1 fields -->
<button type="button" class="btn btn-primary" id="toStep2">Next →</button>
</div>
<div id="step2" class="d-none">
<!-- Step 2 fields -->
<button type="button" class="btn btn-secondary" id="toStep1">← Back</button>
<button type="submit" class="btn btn-success">Submit</button>
</div>
</div>
<script>
document.getElementById('toStep2').addEventListener('click', () => {
document.getElementById('step1').classList.add('d-none');
document.getElementById('step2').classList.remove('d-none');
document.getElementById('stepAnnouncer').textContent = 'Step 2 of 2';
});
</script>
If you are building marketing landing pages that convert, these modal patterns pair naturally with the conversion-focused sections in Canvas Template. You can also prototype entire flows visually using CanvasBuilder, the AI-powered website builder that generates Bootstrap 5 HTML you can customise immediately.
Frequently Asked Questions
How do I prevent a Bootstrap 5 modal from closing when clicking the backdrop?
Pass backdrop: 'static' when instantiating the modal via JavaScript, or add the attribute data-bs-backdrop="static" to the outer .modal element. Combined with data-bs-keyboard="false" (or keyboard: false in JS), the modal will only close via an explicit dismiss button or programmatic modal.hide() call.
Can I open one Bootstrap modal from inside another?
Technically yes, but nested modals are not officially supported and can cause z-index and scroll-lock conflicts. The recommended pattern is to close the first modal programmatically with modal.hide(), then in the hidden.bs.modal event handler, open the second one. This avoids stacking issues and keeps the ARIA hierarchy clean.
How do I pass dynamic data to a Bootstrap 5 modal?
Use data-* attributes on the trigger element and read them via event.relatedTarget.dataset inside the show.bs.modal event listener. This is the pattern shown in the JavaScript events section above and is the most maintainable approach for table-driven edit dialogs or product detail modals.
Why is my custom modal animation not working?
The most common cause is that Bootstrap’s built-in transform on .modal.fade .modal-dialog is overriding your keyframe. Add animation: none on .modal.fade .modal-dialog first to clear the default, then apply your keyframe on .modal.show .modal-dialog. Also confirm your stylesheet loads after the Bootstrap CSS so your rules have higher specificity.
Does Bootstrap 5 support accessible modal dialogs out of the box?
Bootstrap 5 does a solid job with focus trapping, focus return, and aria-modal — but you are still responsible for providing correct aria-labelledby or aria-label attributes, using role="alertdialog" for destructive confirmations, and honouring prefers-reduced-motion in any custom animations. See our full walkthrough on Bootstrap 5 accessibility for WCAG 2.1 AA for a complete audit checklist.
Conclusion
The bootstrap 5 modal is deceptively powerful. The default fade-in and centred dialog covers 80% of use cases, but the remaining 20% — dynamic data loading, custom animations, responsive fullscreen behaviour, accessible confirmation dialogs — is where your implementation either feels production-grade or looks like it was cobbled together from Stack Overflow snippets.
The patterns in this bootstrap modal tutorial give you a repeatable toolkit: clean anatomy, flexible size modifiers, JavaScript event hooks for async workflows, and CSS keyframes that respect user motion preferences. Combined with a well-structured foundation, these building blocks let you ship polished UI fast.
Ready to build faster? Canvas Template gives you a professionally designed Bootstrap 5 HTML template with dozens of pre-built sections — including modal-powered contact forms, video lightboxes, and confirmation dialogs — that you can customise rather than build from zero. If you want to prototype your next project visually before writing any code, CanvasBuilder generates clean Bootstrap 5 HTML in minutes using AI, giving you a head start you can take straight into production.
Skip the setup — build it free
Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.
Canvas Team
Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.
More from the Canvas Blog