Popper.js (version 2.x, bundled inside bootstrap.bundle.min.js) requires explicit JavaScript initialisation and carries a handful of configuration quirks that produce silent failures when you get them wrong. This guide covers every layer: the required HTML attributes, the full Popper config API, sanitisation rules, accessibility requirements, and the most common reasons a Bootstrap 5 tooltip simply refuses to appear.
Key Takeaways
- Bootstrap 5 tooltips and popovers are opt-in: they will never initialise without explicit JavaScript.
- Popper.js 2 is bundled inside
bootstrap.bundle.min.js. Loading the standalonebootstrap.min.jswithout Popper separately is the single most common cause of tooltips not working. - The
popperConfigoption accepts a plain object or a callback that receives the default config, giving you fine-grained control over placement, modifiers, and offsets. - Bootstrap’s built-in HTML sanitiser will silently strip unrecognised attributes from tooltip and popover content unless you extend the allow-list.
- Dynamic content, hidden containers, and CSS
overflow: hiddenon a parent element are the three most common structural pitfalls.
Why Bootstrap 5 Tooltips Require Explicit Initialisation
Bootstrap 5 removed jQuery as a dependency and no longer auto-initialises components on page load. Every tooltip and popover must be activated through the JavaScript API. The fastest approach selects all elements carrying a data-bs-toggle="tooltip" attribute in a single pass:
<!-- HTML -->
<button
type="button"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Save your progress">
Save
</button><!-- JavaScript (runs after DOM is ready) -->
<script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
</script>If you load bootstrap.min.js rather than bootstrap.bundle.min.js, Popper is absent and bootstrap.Tooltip will throw a silent error. Always use the bundle, or load Popper 2 separately before Bootstrap’s JS file. The bundle targets Popper version 2.11.x, so do not mix it with a manually imported Popper 1.x build.
For popovers the pattern is identical, with data-bs-toggle="popover" and a separate data-bs-content attribute carrying the body text:
<button
type="button"
data-bs-toggle="popover"
data-bs-title="Did you know?"
data-bs-content="Popovers support a header and body.">
Info
</button>
<script>
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(el => {
new bootstrap.Popover(el);
});
</script>
Controlling Placement with popperConfig
Both bootstrap.Tooltip and bootstrap.Popover accept a popperConfig option that is merged with (or replaces) Bootstrap’s internal Popper configuration. You can pass a plain object or a callback. The callback form is safer because it gives you access to Bootstrap’s computed defaults before you modify them:
<script>
new bootstrap.Tooltip(document.getElementById('my-btn'), {
placement: 'right',
popperConfig(defaultConfig) {
return {
...defaultConfig,
modifiers: [
...defaultConfig.modifiers,
{
name: 'offset',
options: { offset: [0, 16] } // [skidding, distance] in px
},
{
name: 'preventOverflow',
options: { boundary: document.getElementById('scroll-container') }
}
]
};
}
});
</script>The offset modifier takes a two-element array: the first value shifts along the reference element’s axis (skidding), the second shifts perpendicular to it (distance). Increasing the distance value is the correct way to add breathing room between a trigger and its tooltip. Setting CSS margin on the tooltip element itself will not work reliably.
The preventOverflow modifier keeps the tooltip inside its boundary. The default boundary is clippingParents, which is almost always what you want. Setting it to a specific scroll container is necessary when the tooltip sits inside a fixed-height, overflowing panel such as a sidebar nav.
The flip modifier is enabled by default and automatically reverses placement when there is no room. You can disable it entirely or restrict the fallback placements:
<script>
new bootstrap.Tooltip(el, {
popperConfig(defaultConfig) {
return {
...defaultConfig,
modifiers: [
...defaultConfig.modifiers.filter(m => m.name !== 'flip'),
{ name: 'flip', options: { fallbackPlacements: ['bottom'] } }
]
};
}
});
</script>Bootstrap’s HTML Sanitiser and the Allow-List
Bootstrap 5 sanitises all HTML passed to the title, content, and html options by default. Any tag or attribute not present in the internal allow-list is stripped silently. This catches developers by surprise when they try to embed a link or icon inside popover content.
To extend the allow-list without disabling sanitisation entirely, pass a custom allowList object. The keys are tag names and the values are arrays of permitted attributes:
<script>
new bootstrap.Popover(el, {
html: true,
content: '<a href="/docs" class="text-primary">Read the docs</a>',
allowList: {
...bootstrap.Tooltip.Default.allowList,
a: ['href', 'class', 'target', 'rel']
}
});
</script>Passing sanitize: false disables all sanitisation. Only do this when the content is entirely developer-controlled and never derived from user input. Unsanitised HTML inside a tooltip is a direct XSS vector.

Common Reasons a Bootstrap 5 Tooltip Is Not Working
When a tooltip silently fails to appear, work through this checklist in order:
- Wrong script file. Confirm the page loads
bootstrap.bundle.min.js, notbootstrap.min.jswithout a separate Popper import. - Initialisation runs before the DOM is ready. Wrap your initialisation loop in a
DOMContentLoadedlistener or place the script tag at the bottom of<body>. - Double-initialisation. Calling
new bootstrap.Tooltip(el)on an element that already has a tooltip instance throws an error and leaves the second call in a broken state. Usebootstrap.Tooltip.getInstance(el)to check first. - Parent has
overflow: hiddenoroverflow: scroll. The tooltip is rendered but clipped. Append it tobodyusingcontainer: 'body'in the options. - Trigger element is disabled. Pointer events on a disabled button are suppressed by the browser. Wrap the button in a
<span>and attach the tooltip to the span. - Content added dynamically after page load. Elements inserted via JavaScript after the initialisation loop are not picked up. Use event delegation or re-run initialisation after inserting new nodes.
- Missing
titleattribute. A tooltip with an empty or absent title renders an invisible zero-height box. Bootstrap does not warn about this.
If performance is a concern, initialising hundreds of tooltip instances at once has a measurable cost. See the discussion on reducing JavaScript parse overhead in the Bootstrap 5 performance guide for strategies like lazy initialisation on mouseenter.
Accessibility Requirements for Tooltips and Popovers
Screen readers do not follow the mouse, so tooltip content must also be conveyed through accessible means. For purely decorative labels that duplicate visible text, a tooltip is acceptable. For content that is not available elsewhere on the page, use a different pattern entirely. The key rules are:
- Tooltips triggered only on
hoverare inaccessible by keyboard. Always includefocusin the trigger list:trigger: 'hover focus'. - Bootstrap automatically sets
role="tooltip"and links it to the trigger element viaaria-describedby. Do not override these attributes manually. - Popovers that contain interactive controls (links, buttons) must be dismissible by keyboard. Bootstrap’s default
dismiss: truebehaviour handles the Escape key, but you must ensure focus can reach the popover content. For complex interactive overlays, a modal or a proper ARIA dialog pattern is more appropriate. - Colour contrast on the default dark tooltip background meets WCAG AA for white body text. If you retheme the tooltip using
--cnvs-themecoloror custom CSS, re-check the contrast ratio.
Using Tooltips and Popovers in the Canvas HTML Template
The Canvas HTML Template includes a pre-built UI elements page that demonstrates all Bootstrap 5 tooltip placements, popover variants, and custom-styled examples. The template loads bootstrap.bundle.min.js through plugins.min.js, so Popper is always available. Tooltip initialisation in Canvas is centralised inside functions.bundle.js, which scans for [data-bs-toggle="tooltip"] and [data-bs-toggle="popover"] automatically at DOMContentLoaded.
If you add a new page outside the standard template structure, you need to either include functions.bundle.js or duplicate the initialisation loop manually. The centralised approach means you can add tooltip attributes to any element in any of Canvas’s 50-plus demo pages and they will work immediately without per-page JS. If you want a non-default Popper config on a specific element, pass it via a data attribute using data-bs-popper-config as a JSON string, which Canvas’s initialisation loop will pick up and merge.
For teams using Canvas across multiple client projects, the CSS variable system makes restyling tooltips straightforward. Background colour, border radius, and padding all respond to CSS custom properties. You can read more about that approach in the post on theming one HTML template for multiple client brands.
Frequently Asked Questions
The most likely cause is loading bootstrap.min.js without Popper, or running the initialisation script before the DOM is ready. Switch to bootstrap.bundle.min.js and ensure your querySelectorAll loop runs inside a DOMContentLoaded listener. Also confirm the trigger element has a non-empty title attribute.
The placement option is a Bootstrap-level shortcut that sets Popper’s placement string to one of the eight standard positions (top, bottom, left, right, and their start or end variants). The popperConfig option gives you direct access to the underlying Popper 2 configuration object, including modifiers, strategy, and onFirstUpdate. Use placement for simple cases and popperConfig when you need offset control, custom boundaries, or modifier overrides.
Yes. Set html: true in the options. Be aware that Bootstrap’s sanitiser will strip tags and attributes not present in the allow-list. Extend the allow-list using the allowList option rather than disabling sanitisation with sanitize: false, which creates an XSS risk if any content originates from user input.
Wrap the disabled button in a block-level or inline-block element such as a <span> with style="display:inline-block" and attach the tooltip to that wrapper element. Disabled form elements suppress pointer events, so the tooltip on the button itself will never receive the hover or focus event needed to trigger it.
Retrieve the existing instance with bootstrap.Tooltip.getInstance(el) and call .show(), .hide(), or .toggle() on it. Creating a new instance on an element that already has one will cause errors. If you need to update the tooltip content at runtime, call instance.setContent({ '.tooltip-inner': 'New text' }) before calling .show().
Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.
If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.
Skip the setup and 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