Overlays: tooltips, popovers and toasts
Configure and trigger tooltips and popovers correctly, build toasts that stack and auto-dismiss, and know when an overlay is the wrong tool.
Tooltips and popovers
Tooltips and popovers are opt-in: nothing is initialised from data attributes alone. They also do not appear on touch devices unless you ask for a different trigger, which is the most common complaint after shipping.
import { Tooltip, Popover } from 'bootstrap';
// initialise every opt-in overlay on the page
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((el) => {
Tooltip.getOrCreateInstance(el, { trigger: 'hover focus' });
});
// popovers need their own pass and usually richer options
document.querySelectorAll('[data-bs-toggle="popover"]').forEach((el) => {
Popover.getOrCreateInstance(el, {
trigger: 'click',
html: true,
sanitize: true, // keep this on for user-supplied content
customClass: 'shadow-sm',
delay: { show: 150, hide: 100 }
});
});<button type="button" class="btn btn-link p-0"
data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Copies to clipboard"
aria-label="Copy to clipboard">
Copy
</button>
<button type="button" class="btn btn-outline-secondary"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Billing period"
data-bs-content="Yearly plans are invoiced once and save two months."
aria-label="Explain billing period">
What's this?
</button>- Tooltips are never a place for essential information: they are unreachable on touch, invisible to keyboard users who never hover, and hidden from screen readers that do not read the title attribute.
- Titles in the DOM come from
titleordata-bs-title. Bootstrap removestitleto stop the native tooltip appearing, sodata-bs-titleis the safer authoring choice. placement: 'auto'lets Popper flip the overlay when it would overflow the viewport; without it, a tooltip near the top of the page gets cut off.- Because tooltips and popovers render into the document body by default,
overflow: hiddenon the trigger's parent does not clip them — a frequent cause of confusion.
Toasts and stacking
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toasts"
aria-live="polite" aria-atomic="true"></div>import { Toast } from 'bootstrap';
const container = document.getElementById('toasts');
function notify(message, { variant = 'success', delay = 5000 } = {}) {
// build, do not query: templates keep the markup in one place
const el = document.createElement('div');
el.className = `toast align-items-center text-bg-${variant} border-0`;
el.setAttribute('role', variant === 'danger' ? 'alert' : 'status');
el.setAttribute('aria-live', variant === 'danger' ? 'assertive' : 'polite');
el.setAttribute('aria-atomic', 'true');
el.innerHTML = `
<div class="d-flex">
<div class="toast-body"></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto"
data-bs-dismiss="toast" aria-label="Close"></button>
</div>`;
el.querySelector('.toast-body').textContent = message; // textContent: never inject HTML here
container.append(el);
const toast = new Toast(el, { delay, autohide: delay > 0 });
// dispose the element when it hides so repeated notifications do not leak nodes
el.addEventListener('hidden.bs.toast', () => { toast.dispose(); el.remove(); }, { once: true });
toast.show();
}
notify('Invoice #1042 saved.');
notify('Could not reach the payment provider.', { variant: 'danger', delay: 0 });⚠️
A container with
aria-live must exist in the DOM before the message is inserted, or screen readers announce nothing. Put the empty container in the server-rendered HTML rather than creating it in JavaScript on demand.Choosing the right overlay
| Situation | Right tool | Why |
|---|---|---|
| Short label for an unfamiliar icon | Visible text or a tooltip | Tooltips hide the answer behind hover |
| Result of an action the user just took | Toast | Non-blocking, appears near the work |
| Confirmation the user must answer | modal | A toast can be missed and dismisses itself |
| Extra detail behind a word | Inline disclosure or popover | Keyboard reachable and copyable |
| Errors that block progress | Inline form feedback | The message belongs next to the field |
| Destructive irreversible action | Modal with typed confirmation | Forces a deliberate decision |
- An overlay never receives focus automatically, so anything interactive inside one has to be reachable another way.
- Toasts stack in DOM order; a fixed container in a corner is predictable. Toasts anchored to the cursor look clever and are hard to click.
- Auto-hiding is a courtesy, not a guarantee: keep the message available elsewhere for anything a user may need to read after it disappears.
FAQ
Why does my tooltip not work on a phone?
The default trigger is
hover focus, neither of which exists on touch. Set trigger: 'click' or an explicit combination such as 'hover focus click', and remember that a touch user now needs a second tap to dismiss it.Do I need to dispose toasts manually?
Yes, if you create them dynamically. The instance holds listeners and Popper state; removing the node without
dispose() leaks it. Listen for hidden.bs.toast and clean up there.Related
Navigation and disclosure patterns The JavaScript API, plugins and events
Last refreshed 2026-09-18.