The JavaScript API, plugins and events
Construct plugins from JavaScript, pass options, listen to framework events, handle dynamically added markup, and avoid the double-initialisation trap.
Constructors and options
// The global build exposes every plugin on the bootstrap namespace.
// The ESM build lets you import only what you ship.
import { Modal, Collapse, Tab } from 'bootstrap';
const modal = new Modal(document.getElementById('confirm'), {
backdrop: 'static', // clicking the backdrop will not close it
keyboard: false, // Escape will not close it
focus: true // move focus into the dialog on open
});
modal.show();
console.log(modal._element, modal._config); // inspect, do not mutate
// Every plugin also exposes a factory that reuses an existing instance:
const same = Modal.getOrCreateInstance(document.getElementById('confirm'));
console.log(same === modal); // true| Static method | Returns | Use for |
|---|---|---|
new Plugin(el, options) | A new instance | When you know the element has no instance yet |
Plugin.getInstance(el) | Instance or null | Reading state without creating anything |
Plugin.getOrCreateInstance(el, opts) | Existing or new instance | The safe default in application code |
instance.dispose() | — | Before removing an element or re-rendering |
instance._config | Options object | Debugging only; there is no public setter |
Instances are stored on the element under a namespaced key, so constructing twice does not create two objects — but it does re-run the constructor and can bind duplicate listeners in plugins that are not strict about it. getOrCreateInstance removes the question.
Framework events
const el = document.getElementById('nav');
// present tense = before, past tense = after. Both bubble.
el.addEventListener('show.bs.collapse', (event) => {
console.log('about to open', event.target);
});
el.addEventListener('shown.bs.collapse', (event) => {
// safe to measure: layout is committed
console.log('height is now', event.target.getBoundingClientRect().height);
});
el.addEventListener('hide.bs.collapse', (event) => {
// preventDefault() vetoes the transition entirely
if (hasUnsavedChanges()) event.preventDefault();
});
// Save and restore scroll position around a modal
const modalEl = document.getElementById('confirm');
let scrollY = 0;
modalEl.addEventListener('show.bs.modal', () => { scrollY = window.scrollY; });
modalEl.addEventListener('hidden.bs.modal', () => {
window.scrollTo({ top: scrollY, behavior: 'instant' });
});- Event names follow
<action>.bs.<plugin>:show.bs.modal,shown.bs.tab,close.bs.alert. - Only the
showfamily is cancelable; theshownfamily fires after the change and cannot be stopped. - Events bubble from the element that owns the plugin, so one listener on a container can observe every card, tab or collapse inside it.
event.relatedTargetonshown.bs.tabholds the tab being left, which is how you cancel a scroll animation that is still running.
💡
preventDefault() on a Bootstrap event stops a visual transition but not your own data change. If a collapse is cancelled, whatever code you wrote to load the panel content must be cancelled too — the two are not connected.Dynamic content and teardown
// Markup injected after load has no listeners bound by data attributes.
// Option A: bind plugins after every insertion.
function hydrate(root = document) {
root.querySelectorAll('[data-bs-toggle="tooltip"]')
.forEach((el) => Tooltip.getOrCreateInstance(el, { trigger: 'hover focus' }));
}
const list = document.getElementById('rows');
list.addEventListener('click', (event) => {
const remove = event.target.closest('[data-remove]');
if (!remove) return;
const row = remove.closest('tr');
const instance = Tooltip.getInstance(remove); // never leave instances on dead nodes
instance?.dispose();
row.remove(); // listeners on the row die with it
});
const observer = new MutationObserver(() => hydrate(list));
observer.observe(list, { childList: true, subtree: true });| Situation | Do | Because |
|---|---|---|
| New markup with data attributes | Call getOrCreateInstance after insert | There is no auto-binding in Bootstrap 5 |
| Removing an element with a plugin | dispose() first | Popper and listeners survive the node otherwise |
| Re-rendering a whole panel | Dispose the container's instances | Otherwise you accumulate a second set of handlers |
Delegated click on data-bs-toggle | Also initialise the target plugin | The data API only covers the trigger, not options |
FAQ
Is the data-attribute API enough for a real app?
It is enough for defaults, which is exactly its limitation. As soon as you need options, cleanup on removal, or programmatic control after an event, switch to the constructor API and treat the data attributes as markup-only hints.
How do I reinitialise a plugin after a framework re-render?
Watch for the re-render, call
dispose() on any instance still attached to the old nodes, then call getOrCreateInstance on the new ones. Never keep an instance reference across a re-render — it points at a detached node.Related
Navigation and disclosure patterns Overlays: tooltips, popovers and toasts
Last refreshed 2026-09-18.