JavaScript Behavior

This section documents Bootstrap 5.x as implemented by the official Bootstrap project. No specific patch version is pinned. Unlike the other reference sections on this site, no single reference book underpins it: the content was generated with the assistance of AI from general knowledge of Bootstrap, and should be verified against the current official documentation at getbootstrap.com/docs before relying on it in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Interactive Bootstrap components — modals, dropdowns, tooltips, offcanvas panels, collapses, carousels, toasts — need JavaScript to open, close, and animate, in addition to the CSS that styles them. Bootstrap 5 exposes two ways to drive that behavior: declarative data-bs-* attributes for the common case, and a full JavaScript API for programmatic control. This page covers both, the library’s dependency model, and the custom events every stateful component dispatches as it changes state.

Declarative behavior via data-bs-* attributes

For the majority of uses — a button that opens a modal, a link that toggles a collapse — no JavaScript needs to be written at all. Bootstrap’s own bundled script watches the document for clicks on elements carrying data-bs-toggle, and wires up the target component automatically:

<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
  Launch modal
</button>

<div class="modal" id="exampleModal" tabindex="-1">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Modal title</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">Modal body content.</div>
    </div>
  </div>
</div>

data-bs-toggle="modal" identifies the component type, data-bs-target="#exampleModal" identifies which instance of it, and data-bs-dismiss="modal" on the close button closes it — three attributes, zero lines of custom script. The same pattern covers every other stateful component:

<!-- Dropdown -->
<button class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown">Menu</button>

<!-- Collapse -->
<button class="btn" data-bs-toggle="collapse" data-bs-target="#details">Details</button>
<div class="collapse" id="details">Collapsible content.</div>

<!-- Tooltip -- requires explicit JS initialization, see below -->
<button data-bs-toggle="tooltip" data-bs-title="Helpful text">Hover me</button>

Tooltips and popovers are a deliberate exception: for performance, they are not auto-initialized on page load the way modals and collapses are (instantiating one per element would be wasteful on a page with many candidates), so they still need one explicit JavaScript call, shown below, even though they use the same data-bs-* attribute convention for their configuration.

Programmatic control via the JavaScript API

Every component that supports the data-bs-* pattern is backed by a JavaScript class of the same name — bootstrap.Modal, bootstrap.Dropdown, bootstrap.Collapse, bootstrap.Tooltip, and so on — constructible directly for cases the declarative attributes can’t express: opening a modal in response to a fetch completing, showing a toast from an error handler, or initializing every tooltip on a page in one pass.

import { Modal, Tooltip } from "bootstrap";

// Open a modal programmatically, e.g. after an async operation
const modalEl = document.getElementById("exampleModal");
const modal = new Modal(modalEl);
modal.show();

// Explicit tooltip initialization -- required, not automatic
const tooltipTriggers = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggers.forEach((el) => new Tooltip(el));

getOrCreateInstance() is the safe way to retrieve a component instance that may already have been created (by the declarative attribute path or an earlier script), instead of risking a second, conflicting instance on the same element:

import { Modal } from "bootstrap";

const modalEl = document.getElementById("exampleModal");
const modal = Modal.getOrCreateInstance(modalEl);
modal.hide();

Every component exposes the same small, predictable method surface — typically show()/hide()/toggle() and dispose() to tear down the instance and detach its listeners — so switching from the declarative attributes to the JS API for one particular interaction doesn’t require learning a different vocabulary.

Dependencies: no jQuery, vanilla JS plus Popper.js

Bootstrap 4 and earlier required jQuery as a hard dependency for every interactive component. Bootstrap 5 dropped it entirely — every component is implemented in plain, framework-free JavaScript, which removes a sizeable dependency and its associated overhead for a project that doesn’t otherwise need jQuery.

The one dependency Bootstrap 5 still pulls in is Popper.js, used exclusively for positioning the floating elements that need to stay attached to a reference element while avoiding the viewport edge: dropdown menus, tooltips, and popovers. Popper is not needed for modals, collapses, offcanvas panels, toasts, or carousels, none of which have to dynamically reposition themselves relative to a trigger.

Two build outputs reflect this split:

File Contents

bootstrap.js / bootstrap.esm.js

Every Bootstrap component, without Popper bundled in — use this if Popper is already loaded separately (e.g. as its own dependency, or shared with another library on the page).

bootstrap.bundle.js / bootstrap.bundle.min.js

The same, with Popper bundled in — the simplest choice for a project with no other reason to manage Popper as a separate dependency.

<!-- Simplest path: one script tag, Popper included -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

Component events

Every stateful component dispatches a pair of custom DOM events around each state transition — one just before the change starts, one just after it finishes (including its CSS transition, where applicable) — following the naming convention {event}.bs.{component}:

const modalEl = document.getElementById("exampleModal");

modalEl.addEventListener("show.bs.modal", (event) => {
  // fires immediately when show() is called, before the modal is visible
  console.log("Modal about to open", event.relatedTarget);
});

modalEl.addEventListener("shown.bs.modal", () => {
  // fires once the modal is fully visible and its CSS transition has completed
  document.getElementById("modal-input")?.focus();
});

modalEl.addEventListener("hidden.bs.modal", () => {
  // fires once the modal has fully closed -- safe point to reset form state
  modalEl.querySelector("form")?.reset();
});

The -ing/-ed-style pairing (show / shown, hide / hidden) is consistent across every component that has an open/close lifecycle — show.bs.collapse/shown.bs.collapse, show.bs.dropdown/shown.bs.dropdown, show.bs.offcanvas/shown.bs.offcanvas — which is what makes it practical to hook a single generic listener pattern (e.g. autofocusing the first input) across every dismissible component in a project rather than writing one-off logic per component type. The "before" event (show.bs.modal) is also cancelable via event.preventDefault(), letting application code veto the transition — for example, blocking a modal from opening until an unsaved-changes check passes.

Event flow: from a click to a dispatched event

flowchart LR CLICK["User clicks an element
with data-bs-toggle=modal"] PARSE["Bootstrap's delegated
click listener parses
data-bs-* attributes"] INSTANCE["Modal.getOrCreateInstance()
on the target element"] SHOWEVT["show.bs.modal dispatched
(cancelable)"] DOM["DOM updated:
display set, backdrop inserted,
CSS transition runs"] SHOWNEVT["shown.bs.modal dispatched
(transition complete)"] CLICK --> PARSE --> INSTANCE --> SHOWEVT --> DOM --> SHOWNEVT classDef core fill:#3f51b5,stroke:#1a237e,color:#fff class INSTANCE core

The same flow applies whether the trigger was a data-bs-toggle click or a direct new Modal(el).show() call — the declarative attribute path is a thin layer that ends up calling the identical JavaScript API underneath, which is why both approaches dispatch the same show.bs.modal/shown.bs.modal events and can be mixed freely within one project.