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 methodReturnsUse for
new Plugin(el, options)A new instanceWhen you know the element has no instance yet
Plugin.getInstance(el)Instance or nullReading state without creating anything
Plugin.getOrCreateInstance(el, opts)Existing or new instanceThe safe default in application code
instance.dispose()Before removing an element or re-rendering
instance._configOptions objectDebugging 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 show family is cancelable; the shown family 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.relatedTarget on shown.bs.tab holds 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 });
SituationDoBecause
New markup with data attributesCall getOrCreateInstance after insertThere is no auto-binding in Bootstrap 5
Removing an element with a plugindispose() firstPopper and listeners survive the node otherwise
Re-rendering a whole panelDispose the container's instancesOtherwise you accumulate a second set of handlers
Delegated click on data-bs-toggleAlso initialise the target pluginThe 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.

Navigation and disclosure patterns Overlays: tooltips, popovers and toasts

Last refreshed 2026-09-18.