Every Bootstrap plugin follows the same design: a class per component, one instance per DOM element, options that can come from data attributes or a constructor argument, and a set of events named {action}.bs.{component}. Once you know this pattern you can drive any component from code, integrate Bootstrap with frameworks, and fix the classic "modal opens twice" bug. This lesson lays out the shared API, then shows how it applies across components.
Three ways, depending on the project:
<!-- 1. Global bundle: everything on window.bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script>
const modal = new bootstrap.Modal("#example");
</script>// 2. ES modules with a bundler
import { Modal, Tooltip, Toast } from "bootstrap";
// 3. Individual plugin files for the smallest bundle
import Modal from "bootstrap/js/dist/modal";Bootstrap also ships bootstrap.esm.min.js for <script type="module"> usage without a bundler; in that case Popper must be resolved through an import map.
Every constructor accepts an element or a CSS selector, plus an optional options object:
new Modal(document.getElementById("example"));
new Modal("#example");
new Modal("#example", { backdrop: "static" });Options are resolved in this order: plugin defaults, then data-bs-* attributes on the element, then the constructor argument. Attribute names are the kebab-case form of the option: data-bs-auto-close="outside" equals { autoClose: "outside" }.
A plugin keeps one instance per element. Creating a second one with new on the same element returns the existing instance, but the cleaner methods are:
| Method | Behavior |
|---|---|
| Plugin.getInstance(el) | Returns the instance or null if none exists |
| Plugin.getOrCreateInstance(el, options) | Returns the instance, creating it with options if needed |
const dd = Dropdown.getInstance("#userMenu"); // null if never initialized
Dropdown.getOrCreateInstance("#userMenu").show();getOrCreateInstance is what you want almost every time; it is safe to call from any event handler without tracking state.
Most plugins share a small vocabulary:
show(), hide(), toggle(): change visibility (modal, offcanvas, collapse, dropdown, toast, tooltip, popover, tab).enable(), disable(), toggleEnabled(): tooltips and popovers.update(): recompute Popper position (dropdown, tooltip, popover) or modal layout after content changes.dispose(): remove event listeners and data so the element can be safely deleted.handleUpdate(): modal-specific re-layout when the height changes while open.Method calls are asynchronous in the sense that they start a transition; calling show() and then hide() immediately is ignored until the transition ends.
Events fire on the component's root element (the toggle for dropdowns and tabs, the modal for modals) and bubble, so delegation works. Four-event lifecycle:
| Event | Timing | Cancelable |
|---|---|---|
| show.bs.* | Before showing | Yes |
| shown.bs.* | After the transition | No |
| hide.bs.* | Before hiding | Yes |
| hidden.bs.* | After hidden | No |
const modalEl = document.getElementById("editor");
modalEl.addEventListener("hide.bs.modal", (event) => {
if (hasUnsavedChanges() && !window.confirm("Discard changes?")) {
event.preventDefault(); // keeps the modal open
}
});
modalEl.addEventListener("shown.bs.modal", () => {
modalEl.querySelector("input").focus();
});Component-specific events add to this list: slide.bs.carousel / slid.bs.carousel, activate.bs.scrollspy, inserted.bs.tooltip, and hidePrevented.bs.modal when a static-backdrop modal refuses to close.
| Component | Notable options |
|---|---|
| Modal | backdrop (true/false/"static"), keyboard, focus |
| Offcanvas | backdrop, keyboard, scroll |
| Tooltip / Popover | trigger, placement, html, sanitize, container, delay, customClass |
| Dropdown | autoClose, boundary, display, offset, reference |
| Toast | animation, autohide, delay |
| Carousel | interval, ride, wrap, touch, pause |
| Collapse | toggle, parent |
| ScrollSpy | target, rootMargin, threshold, smoothScroll |
Read the defaults at runtime with Modal.Default.
In React or Vue, the framework owns the DOM, so initialize plugins after mount and dispose before unmount:
// React
useEffect(() => {
const instance = Modal.getOrCreateInstance(ref.current);
return () => instance.dispose();
}, []);Never let the framework re-render markup that a plugin has moved or cloned (tooltips and popovers append elements to body by default). For large apps, consider the framework-specific ports of Bootstrap, but the plain API works well for a handful of components.
Tooltips and popovers that set html: true run content through a built-in sanitizer with an allowlist of tags and attributes. Extend it with the allowList option or disable sanitization with sanitize: false only for content you generated yourself.
new Modal(el) in a click handler. Repeated clicks stack instances' listeners; use getOrCreateInstance.dispose(). Leaks listeners and, for tooltips, leaves orphaned elements in body.Which method returns an existing plugin instance or creates one if needed?
data-bs-* attributes, then the constructor.getOrCreateInstance in handlers and dispose() before removing elements.show, hide, toggle, update and dispose; check each component for extras.show/shown/hide/hidden.bs.{component}, fire on the component's root or toggle, and the pre-transition events are cancelable.Next lesson: RTL, Accessibility and Migrating from Bootstrap 4 - right-to-left builds, the accessibility checklist, and what changed from v4 to v5.