},
{
"path": "./dist/js/bootstrap.bundle.js",
- "maxSize": "43.0 kB"
+ "maxSize": "44.0 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
},
{
"path": "./dist/js/bootstrap.esm.js",
- "maxSize": "28.0 kB"
+ "maxSize": "30.0 kB"
},
{
"path": "./dist/js/bootstrap.esm.min.js",
},
{
"path": "./dist/js/bootstrap.js",
- "maxSize": "28.75 kB"
+ "maxSize": "30.5 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
- "maxSize": "16.25 kB"
+ "maxSize": "16.5 kB"
}
],
"ci": {
configPropertyName: 'js_bundle_hash'
},
{
- file: 'node_modules/@popperjs/core/dist/umd/popper.min.js',
- configPropertyName: 'popper_hash'
+ file: 'node_modules/@floating-ui/dom/dist/floating-ui.dom.umd.min.js',
+ configPropertyName: 'floating_ui_hash'
}
]
const ESM = process.env.ESM === 'true'
let destinationFile = `bootstrap${ESM ? '.esm' : ''}`
-const external = ['@popperjs/core']
+const external = ['@floating-ui/dom']
const plugins = [
babel({
// Only transpile our source code
})
]
const globals = {
- '@popperjs/core': 'Popper'
+ '@floating-ui/dom': 'FloatingUIDOM'
}
if (BUNDLE) {
destinationFile += '.bundle'
- // Remove last entry in external array to bundle Popper
+ // Remove last entry in external array to bundle Floating UI
external.pop()
- delete globals['@popperjs/core']
+ delete globals['@floating-ui/dom']
plugins.push(
replace({
'process.env.NODE_ENV': '"production"',
cdn:
# See https://www.srihash.org for info on how to generate the hashes
css: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
- css_hash: "sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
+ css_hash: "sha384-TDmpFhAO5TwSQwPF95I/odgwpTUuv0aaVm9/0fL7b+kKe7hFBp/+9cBCMkydgGOi"
js: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.min.js"
- js_hash: "sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y"
+ js_hash: "sha384-Php492snRLTR5p+hMyxpV6gYwp1avWXn4AaX31MgANrvsjr9Dpodl3Nw60L7Pewl"
js_bundle: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
- js_bundle_hash: "sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
- popper: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
- popper_hash: "sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
- popper_esm: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/esm/popper.min.js"
+ js_bundle_hash: "sha384-I2J4jlw924JZXHU9un9Mcuixq/rKhd5A8/B1NQ6ifPAiBFacZjwNcec8d6L38jQv"
+ floating_ui: "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.7.0/dist/floating-ui.dom.umd.min.js"
+ floating_ui_hash: "sha384-R7p1RqabZNhI+RdPNIzTouzd/LBVorZ0Tn3ApcogSOk+HF3o+P0HIenrUw/n0MOj"
+ floating_ui_esm: "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.7.0/dist/floating-ui.dom.esm.min.js"
anchors:
min: 2
* --------------------------------------------------------------------------
*/
-import * as Popper from '@popperjs/core'
+import {
+ computePosition,
+ flip,
+ shift,
+ offset,
+ autoUpdate
+} from '@floating-ui/dom'
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
isVisible,
noop
} from './util/index.js'
+import {
+ parseResponsivePlacement,
+ getResponsivePlacement,
+ createBreakpointListeners,
+ disposeBreakpointListeners
+} from './util/floating-ui.js'
/**
* Constants
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_SHOW = 'show'
-const CLASS_NAME_DROPUP = 'dropup'
-const CLASS_NAME_DROPEND = 'dropend'
-const CLASS_NAME_DROPSTART = 'dropstart'
-const CLASS_NAME_DROPUP_CENTER = 'dropup-center'
-const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'
const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`
const SELECTOR_MENU = '.dropdown-menu'
-const SELECTOR_NAVBAR = '.navbar'
const SELECTOR_NAVBAR_NAV = '.navbar-nav'
const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
-const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'
-const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'
-const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'
-const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'
-const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'
-const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'
-const PLACEMENT_TOPCENTER = 'top'
-const PLACEMENT_BOTTOMCENTER = 'bottom'
+// Default placement with RTL support
+const DEFAULT_PLACEMENT = isRTL() ? 'bottom-end' : 'bottom-start'
const Default = {
autoClose: true,
boundary: 'clippingParents',
display: 'dynamic',
offset: [0, 2],
- popperConfig: null,
+ floatingConfig: null,
+ placement: DEFAULT_PLACEMENT,
reference: 'toggle'
}
boundary: '(string|element)',
display: 'string',
offset: '(array|string|function)',
- popperConfig: '(null|object|function)',
+ floatingConfig: '(null|object|function)',
+ placement: 'string',
reference: '(string|element|object)'
}
class Dropdown extends BaseComponent {
constructor(element, config) {
+ if (typeof computePosition === 'undefined') {
+ throw new TypeError('Bootstrap\'s dropdowns require Floating UI (https://floating-ui.com)')
+ }
+
super(element, config)
- this._popper = null
+ this._floatingCleanup = null
+ this._mediaQueryListeners = []
+ this._responsivePlacements = null
this._parent = this._element.parentNode // dropdown wrapper
// TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||
SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||
SelectorEngine.findOne(SELECTOR_MENU, this._parent)
- this._inNavbar = this._detectNavbar()
+
+ // Parse responsive placements on init
+ this._parseResponsivePlacements()
}
// Getters
return
}
- this._createPopper()
+ this._createFloating()
// If this is a touch-enabled device we add extra
// empty mouseover listeners to the body's immediate children;
}
dispose() {
- if (this._popper) {
- this._popper.destroy()
- }
-
+ this._disposeFloating()
+ this._disposeMediaQueryListeners()
super.dispose()
}
update() {
- this._inNavbar = this._detectNavbar()
- if (this._popper) {
- this._popper.update()
+ if (this._floatingCleanup) {
+ this._updateFloatingPosition()
}
}
}
}
- if (this._popper) {
- this._popper.destroy()
- }
+ this._disposeFloating()
this._menu.classList.remove(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOW)
this._element.setAttribute('aria-expanded', 'false')
- Manipulator.removeDataAttribute(this._menu, 'popper')
+ Manipulator.removeDataAttribute(this._menu, 'placement')
+ Manipulator.removeDataAttribute(this._menu, 'display')
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
}
if (typeof config.reference === 'object' && !isElement(config.reference) &&
typeof config.reference.getBoundingClientRect !== 'function'
) {
- // Popper virtual elements require a getBoundingClientRect method
+ // Floating UI virtual elements require a getBoundingClientRect method
throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
}
return config
}
- _createPopper() {
- if (typeof Popper === 'undefined') {
- throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)')
+ _createFloating() {
+ if (this._config.display === 'static') {
+ Manipulator.setDataAttribute(this._menu, 'display', 'static')
+ return
}
let referenceElement = this._element
referenceElement = this._config.reference
}
- const popperConfig = this._getPopperConfig()
- this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
- }
+ // Initial position update
+ this._updateFloatingPosition(referenceElement)
- _isShown() {
- return this._menu.classList.contains(CLASS_NAME_SHOW)
+ // Set up auto-update for scroll/resize
+ this._floatingCleanup = autoUpdate(
+ referenceElement,
+ this._menu,
+ () => this._updateFloatingPosition(referenceElement)
+ )
}
- _getPlacement() {
- const parentDropdown = this._parent
-
- if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {
- return PLACEMENT_RIGHT
+ async _updateFloatingPosition(referenceElement = null) {
+ // Check if menu exists and is still in the DOM
+ if (!this._menu || !this._menu.isConnected) {
+ return
}
- if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {
- return PLACEMENT_LEFT
+ if (!referenceElement) {
+ if (this._config.reference === 'parent') {
+ referenceElement = this._parent
+ } else if (isElement(this._config.reference)) {
+ referenceElement = getElement(this._config.reference)
+ } else if (typeof this._config.reference === 'object') {
+ referenceElement = this._config.reference
+ } else {
+ referenceElement = this._element
+ }
}
- if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {
- return PLACEMENT_TOPCENTER
+ const placement = this._getPlacement()
+ const middleware = this._getFloatingMiddleware()
+ const floatingConfig = this._getFloatingConfig(placement, middleware)
+
+ const { x, y, placement: finalPlacement } = await computePosition(
+ referenceElement,
+ this._menu,
+ floatingConfig
+ )
+
+ // Menu may have been disposed during the async computePosition call
+ if (!this._menu || !this._menu.isConnected) {
+ return
}
- if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {
- return PLACEMENT_BOTTOMCENTER
+ // Apply position to dropdown menu
+ Object.assign(this._menu.style, {
+ position: 'absolute',
+ left: `${x}px`,
+ top: `${y}px`,
+ margin: '0'
+ })
+
+ // Set placement attribute for CSS styling
+ Manipulator.setDataAttribute(this._menu, 'placement', finalPlacement)
+ }
+
+ _isShown() {
+ return this._menu.classList.contains(CLASS_NAME_SHOW)
+ }
+
+ _getPlacement() {
+ // If we have responsive placements, find the appropriate one for current viewport
+ if (this._responsivePlacements) {
+ return getResponsivePlacement(this._responsivePlacements, DEFAULT_PLACEMENT)
}
- // We need to trim the value because custom properties can also include spaces
- const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'
+ return this._config.placement
+ }
- if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {
- return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP
+ _parseResponsivePlacements() {
+ this._responsivePlacements = parseResponsivePlacement(this._config.placement, DEFAULT_PLACEMENT)
+
+ if (this._responsivePlacements) {
+ this._setupMediaQueryListeners()
}
+ }
- return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM
+ _setupMediaQueryListeners() {
+ this._disposeMediaQueryListeners()
+ this._mediaQueryListeners = createBreakpointListeners(() => {
+ if (this._isShown()) {
+ this._updateFloatingPosition()
+ }
+ })
}
- _detectNavbar() {
- return this._element.closest(SELECTOR_NAVBAR) !== null
+ _disposeMediaQueryListeners() {
+ disposeBreakpointListeners(this._mediaQueryListeners)
+ this._mediaQueryListeners = []
}
_getOffset() {
}
if (typeof offset === 'function') {
- return popperData => offset(popperData, this._element)
+ // Floating UI passes different args, adapt the interface for offset function callbacks
+ return ({ placement, rects }) => {
+ const result = offset({ placement, reference: rects.reference, floating: rects.floating }, this._element)
+ return result
+ }
}
return offset
}
- _getPopperConfig() {
- const defaultBsPopperConfig = {
- placement: this._getPlacement(),
- modifiers: [{
- name: 'preventOverflow',
- options: {
- boundary: this._config.boundary
- }
- },
- {
- name: 'offset',
- options: {
- offset: this._getOffset()
- }
- }]
+ _getFloatingMiddleware() {
+ const offsetValue = this._getOffset()
+
+ const middleware = [
+ // Offset middleware - handles distance from reference
+ offset(
+ typeof offsetValue === 'function' ?
+ offsetValue :
+ { mainAxis: offsetValue[1] || 0, crossAxis: offsetValue[0] || 0 }
+ ),
+ // Flip middleware - handles fallback placements
+ flip({
+ fallbackPlacements: this._getFallbackPlacements()
+ }),
+ // Shift middleware - prevents overflow
+ shift({
+ boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary
+ })
+ ]
+
+ return middleware
+ }
+
+ _getFallbackPlacements() {
+ // Get appropriate fallback placements based on current placement
+ // Fallbacks should preserve alignment (start/end) when possible
+ const placement = this._getPlacement()
+
+ // Handle all possible Floating UI placements
+ const fallbackMap = {
+ bottom: ['top', 'bottom-start', 'bottom-end', 'top-start', 'top-end'],
+ 'bottom-start': ['top-start', 'bottom-end', 'top-end'],
+ 'bottom-end': ['top-end', 'bottom-start', 'top-start'],
+ top: ['bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end'],
+ 'top-start': ['bottom-start', 'top-end', 'bottom-end'],
+ 'top-end': ['bottom-end', 'top-start', 'bottom-start'],
+ right: ['left', 'right-start', 'right-end', 'left-start', 'left-end'],
+ 'right-start': ['left-start', 'right-end', 'left-end', 'top-start', 'bottom-start'],
+ 'right-end': ['left-end', 'right-start', 'left-start', 'top-end', 'bottom-end'],
+ left: ['right', 'left-start', 'left-end', 'right-start', 'right-end'],
+ 'left-start': ['right-start', 'left-end', 'right-end', 'top-start', 'bottom-start'],
+ 'left-end': ['right-end', 'left-start', 'right-start', 'top-end', 'bottom-end']
}
- // Disable Popper if we have a static display or Dropdown is in Navbar
- if (this._inNavbar || this._config.display === 'static') {
- Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove
- defaultBsPopperConfig.modifiers = [{
- name: 'applyStyles',
- enabled: false
- }]
+ return fallbackMap[placement] || ['top', 'bottom', 'right', 'left']
+ }
+
+ _getFloatingConfig(placement, middleware) {
+ const defaultConfig = {
+ placement,
+ middleware
}
return {
- ...defaultBsPopperConfig,
- ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
+ ...defaultConfig,
+ ...execute(this._config.floatingConfig, [undefined, defaultConfig])
+ }
+ }
+
+ _disposeFloating() {
+ if (this._floatingCleanup) {
+ this._floatingCleanup()
+ this._floatingCleanup = null
}
}
*/
import Tooltip from './tooltip.js'
+import EventHandler from './dom/event-handler.js'
/**
* Constants
const SELECTOR_TITLE = '.popover-header'
const SELECTOR_CONTENT = '.popover-body'
+const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="popover"]'
+
+const EVENT_CLICK = 'click'
+const EVENT_FOCUSIN = 'focusin'
+const EVENT_MOUSEENTER = 'mouseenter'
const Default = {
...Tooltip.Default,
}
}
+/**
+ * Data API implementation - auto-initialize popovers
+ */
+
+const initPopover = event => {
+ const target = event.target.closest(SELECTOR_DATA_TOGGLE)
+ if (!target) {
+ return
+ }
+
+ // Prevent default for click events to avoid navigation
+ if (event.type === 'click') {
+ event.preventDefault()
+ }
+
+ // Get or create instance
+ const popover = Popover.getOrCreateInstance(target)
+
+ // Trigger the appropriate action based on event type
+ if (event.type === 'click') {
+ popover.toggle()
+ } else if (event.type === 'focusin') {
+ popover._activeTrigger.focus = true
+ popover._enter()
+ }
+}
+
+// Support click (default), hover, and focus triggers
+EventHandler.on(document, EVENT_CLICK, SELECTOR_DATA_TOGGLE, initPopover)
+EventHandler.on(document, EVENT_FOCUSIN, SELECTOR_DATA_TOGGLE, initPopover)
+EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE, initPopover)
+
export default Popover
* --------------------------------------------------------------------------
*/
-import * as Popper from '@popperjs/core'
+import {
+ computePosition,
+ flip,
+ shift,
+ offset,
+ arrow,
+ autoUpdate
+} from '@floating-ui/dom'
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
} from './util/index.js'
import { DefaultAllowlist } from './util/sanitizer.js'
import TemplateFactory from './util/template-factory.js'
+import {
+ parseResponsivePlacement,
+ getResponsivePlacement,
+ createBreakpointListeners,
+ disposeBreakpointListeners
+} from './util/floating-ui.js'
/**
* Constants
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
+const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tooltip"]'
const EVENT_MODAL_HIDE = 'hide.bs.modal'
html: false,
offset: [0, 6],
placement: 'top',
- popperConfig: null,
+ floatingConfig: null,
sanitize: true,
sanitizeFn: null,
selector: false,
html: 'boolean',
offset: '(array|string|function)',
placement: '(string|function)',
- popperConfig: '(null|object|function)',
+ floatingConfig: '(null|object|function)',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
selector: '(string|boolean)',
class Tooltip extends BaseComponent {
constructor(element, config) {
- if (typeof Popper === 'undefined') {
- throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)')
+ if (typeof computePosition === 'undefined') {
+ throw new TypeError('Bootstrap\'s tooltips require Floating UI (https://floating-ui.com)')
}
super(element, config)
this._timeout = 0
this._isHovered = null
this._activeTrigger = {}
- this._popper = null
+ this._floatingCleanup = null
this._templateFactory = null
this._newContent = null
+ this._mediaQueryListeners = []
+ this._responsivePlacements = null
// Protected
this.tip = null
+ this._parseResponsivePlacements()
this._setListeners()
if (!this._config.selector) {
this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))
}
- this._disposePopper()
+ this._disposeFloating()
+ this._disposeMediaQueryListeners()
super.dispose()
}
- show() {
+ async show() {
if (this._element.style.display === 'none') {
throw new Error('Please use show on visible elements')
}
return
}
- // TODO: v6 remove this or make it optional
- this._disposePopper()
+ this._disposeFloating()
const tip = this._getTipElement()
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
}
- this._popper = this._createPopper(tip)
+ await this._createFloating(tip)
tip.classList.add(CLASS_NAME_SHOW)
}
if (!this._isHovered) {
- this._disposePopper()
+ this._disposeFloating()
}
this._element.removeAttribute('aria-describedby')
}
update() {
- if (this._popper) {
- this._popper.update()
+ if (this._floatingCleanup && this.tip) {
+ this._updateFloatingPosition()
}
}
setContent(content) {
this._newContent = content
if (this._isShown()) {
- this._disposePopper()
+ this._disposeFloating()
this.show()
}
}
return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)
}
- _createPopper(tip) {
+ _getPlacement(tip) {
+ // If we have responsive placements, get the one for current viewport
+ if (this._responsivePlacements) {
+ const placement = getResponsivePlacement(this._responsivePlacements, 'top')
+ return AttachmentMap[placement.toUpperCase()] || placement
+ }
+
+ // Execute placement (can be a function)
const placement = execute(this._config.placement, [this, tip, this._element])
- const attachment = AttachmentMap[placement.toUpperCase()]
- return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
+ return AttachmentMap[placement.toUpperCase()] || placement
+ }
+
+ _parseResponsivePlacements() {
+ // Only parse if placement is a string (not a function)
+ if (typeof this._config.placement !== 'string') {
+ this._responsivePlacements = null
+ return
+ }
+
+ this._responsivePlacements = parseResponsivePlacement(this._config.placement, 'top')
+
+ if (this._responsivePlacements) {
+ this._setupMediaQueryListeners()
+ }
+ }
+
+ _setupMediaQueryListeners() {
+ this._disposeMediaQueryListeners()
+ this._mediaQueryListeners = createBreakpointListeners(() => {
+ if (this._isShown()) {
+ this._updateFloatingPosition()
+ }
+ })
+ }
+
+ _disposeMediaQueryListeners() {
+ disposeBreakpointListeners(this._mediaQueryListeners)
+ this._mediaQueryListeners = []
+ }
+
+ async _createFloating(tip) {
+ const placement = this._getPlacement(tip)
+ const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`)
+
+ // Initial position update
+ await this._updateFloatingPosition(tip, placement, arrowElement)
+
+ // Set up auto-update for scroll/resize
+ this._floatingCleanup = autoUpdate(
+ this._element,
+ tip,
+ () => this._updateFloatingPosition(tip, null, arrowElement)
+ )
+ }
+
+ async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) {
+ if (!tip) {
+ return
+ }
+
+ if (!placement) {
+ placement = this._getPlacement(tip)
+ }
+
+ if (!arrowElement) {
+ arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`)
+ }
+
+ const middleware = this._getFloatingMiddleware(arrowElement)
+ const floatingConfig = this._getFloatingConfig(placement, middleware)
+
+ const { x, y, placement: finalPlacement, middlewareData } = await computePosition(
+ this._element,
+ tip,
+ floatingConfig
+ )
+
+ // Apply position to tooltip
+ Object.assign(tip.style, {
+ position: 'absolute',
+ left: `${x}px`,
+ top: `${y}px`
+ })
+
+ // Ensure arrow is absolutely positioned within tooltip
+ if (arrowElement) {
+ arrowElement.style.position = 'absolute'
+ }
+
+ // Set placement attribute for CSS arrow styling
+ Manipulator.setDataAttribute(tip, 'placement', finalPlacement)
+
+ // Position arrow along the edge (center it) if present
+ // The CSS handles which edge to place it on via data-bs-placement
+ if (arrowElement && middlewareData.arrow) {
+ const { x: arrowX, y: arrowY } = middlewareData.arrow
+ const isVertical = finalPlacement.startsWith('top') || finalPlacement.startsWith('bottom')
+
+ // Only set the cross-axis position (centering along the edge)
+ // The main-axis position (which edge) is handled by CSS
+ Object.assign(arrowElement.style, {
+ left: isVertical && arrowX !== null ? `${arrowX}px` : '',
+ top: !isVertical && arrowY !== null ? `${arrowY}px` : '',
+ // Reset the other axis to let CSS handle it
+ right: '',
+ bottom: ''
+ })
+ }
}
_getOffset() {
}
if (typeof offset === 'function') {
- return popperData => offset(popperData, this._element)
+ // Floating UI passes different args, adapt the interface for offset function callbacks
+ return ({ placement, rects }) => {
+ const result = offset({ placement, reference: rects.reference, floating: rects.floating }, this._element)
+ return result
+ }
}
return offset
return execute(arg, [this._element, this._element])
}
- _getPopperConfig(attachment) {
- const defaultBsPopperConfig = {
- placement: attachment,
- modifiers: [
- {
- name: 'flip',
- options: {
- fallbackPlacements: this._config.fallbackPlacements
- }
- },
- {
- name: 'offset',
- options: {
- offset: this._getOffset()
- }
- },
- {
- name: 'preventOverflow',
- options: {
- boundary: this._config.boundary
- }
- },
- {
- name: 'arrow',
- options: {
- element: `.${this.constructor.NAME}-arrow`
- }
- },
- {
- name: 'preSetPlacement',
- enabled: true,
- phase: 'beforeMain',
- fn: data => {
- // Pre-set Popper's placement attribute in order to read the arrow sizes properly.
- // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement
- this._getTipElement().setAttribute('data-popper-placement', data.state.placement)
- }
- }
- ]
+ _getFloatingMiddleware(arrowElement) {
+ const offsetValue = this._getOffset()
+
+ const middleware = [
+ // Offset middleware - handles distance from reference
+ offset(
+ typeof offsetValue === 'function' ?
+ offsetValue :
+ { mainAxis: offsetValue[1] || 0, crossAxis: offsetValue[0] || 0 }
+ ),
+ // Flip middleware - handles fallback placements
+ flip({
+ fallbackPlacements: this._config.fallbackPlacements
+ }),
+ // Shift middleware - prevents overflow
+ shift({
+ boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary
+ })
+ ]
+
+ // Arrow middleware - positions the arrow element
+ if (arrowElement) {
+ middleware.push(arrow({ element: arrowElement }))
+ }
+
+ return middleware
+ }
+
+ _getFloatingConfig(placement, middleware) {
+ const defaultConfig = {
+ placement,
+ middleware
}
return {
- ...defaultBsPopperConfig,
- ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
+ ...defaultConfig,
+ ...execute(this._config.floatingConfig, [undefined, defaultConfig])
}
}
return config
}
- _disposePopper() {
- if (this._popper) {
- this._popper.destroy()
- this._popper = null
+ _disposeFloating() {
+ if (this._floatingCleanup) {
+ this._floatingCleanup()
+ this._floatingCleanup = null
}
if (this.tip) {
}
}
}
+
+/**
+ * Data API implementation - auto-initialize tooltips
+ */
+
+const initTooltip = event => {
+ const target = event.target.closest(SELECTOR_DATA_TOGGLE)
+ if (!target) {
+ return
+ }
+
+ // Get or create instance and trigger the appropriate action
+ const tooltip = Tooltip.getOrCreateInstance(target)
+
+ // For focus events, manually trigger enter to show
+ if (event.type === 'focusin') {
+ tooltip._activeTrigger.focus = true
+ tooltip._enter()
+ }
+}
+
+EventHandler.on(document, EVENT_FOCUSIN, SELECTOR_DATA_TOGGLE, initTooltip)
+EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE, initTooltip)
+
export default Tooltip
--- /dev/null
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap util/floating-ui.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import { isRTL } from './index.js'
+
+/**
+ * Breakpoints for responsive placement (matches SCSS $grid-breakpoints)
+ */
+export const BREAKPOINTS = {
+ sm: 576,
+ md: 768,
+ lg: 1024,
+ xl: 1280,
+ '2xl': 1536
+}
+
+/**
+ * Default placement with RTL support
+ */
+export const getDefaultPlacement = (fallback = 'bottom') => {
+ if (fallback.includes('-start') || fallback.includes('-end')) {
+ const [side, alignment] = fallback.split('-')
+ const flippedAlignment = alignment === 'start' ? 'end' : 'start'
+ return isRTL() ? `${side}-${flippedAlignment}` : fallback
+ }
+
+ return fallback
+}
+
+/**
+ * Parse a placement string that may contain responsive prefixes
+ * Example: "bottom-start md:top-end lg:right" returns { xs: 'bottom-start', md: 'top-end', lg: 'right' }
+ *
+ * @param {string} placementString - The placement string to parse
+ * @param {string} defaultPlacement - The default placement to use for xs/base
+ * @returns {object|null} - Object with breakpoint keys and placement values, or null if not responsive
+ */
+export const parseResponsivePlacement = (placementString, defaultPlacement = 'bottom') => {
+ // Check if placement contains responsive prefixes (e.g., "bottom-start md:top-end")
+ if (!placementString || !placementString.includes(':')) {
+ return null
+ }
+
+ // Parse the placement string into breakpoint-keyed object
+ const parts = placementString.split(/\s+/)
+ const placements = { xs: defaultPlacement } // Default fallback
+
+ for (const part of parts) {
+ if (part.includes(':')) {
+ // Responsive placement like "md:top-end"
+ const [breakpoint, placement] = part.split(':')
+ if (BREAKPOINTS[breakpoint] !== undefined) {
+ placements[breakpoint] = placement
+ }
+ } else {
+ // Base placement (no prefix = xs/default)
+ placements.xs = part
+ }
+ }
+
+ return placements
+}
+
+/**
+ * Get the active placement for the current viewport width
+ *
+ * @param {object} responsivePlacements - Object with breakpoint keys and placement values
+ * @param {string} defaultPlacement - Fallback placement
+ * @returns {string} - The active placement for current viewport
+ */
+export const getResponsivePlacement = (responsivePlacements, defaultPlacement = 'bottom') => {
+ if (!responsivePlacements) {
+ return defaultPlacement
+ }
+
+ // Get current viewport width
+ const viewportWidth = window.innerWidth
+
+ // Find the largest breakpoint that matches
+ let activePlacement = responsivePlacements.xs || defaultPlacement
+
+ // Check breakpoints in order (sm, md, lg, xl, 2xl)
+ const breakpointOrder = ['sm', 'md', 'lg', 'xl', '2xl']
+
+ for (const breakpoint of breakpointOrder) {
+ const minWidth = BREAKPOINTS[breakpoint]
+ if (viewportWidth >= minWidth && responsivePlacements[breakpoint]) {
+ activePlacement = responsivePlacements[breakpoint]
+ }
+ }
+
+ return activePlacement
+}
+
+/**
+ * Create media query listeners for responsive placement changes
+ *
+ * @param {Function} callback - Callback to run when breakpoint changes
+ * @returns {Array} - Array of { mql, handler } objects for cleanup
+ */
+export const createBreakpointListeners = callback => {
+ const listeners = []
+
+ for (const breakpoint of Object.keys(BREAKPOINTS)) {
+ const minWidth = BREAKPOINTS[breakpoint]
+ const mql = window.matchMedia(`(min-width: ${minWidth}px)`)
+
+ mql.addEventListener('change', callback)
+ listeners.push({ mql, handler: callback })
+ }
+
+ return listeners
+}
+
+/**
+ * Clean up media query listeners
+ *
+ * @param {Array} listeners - Array of { mql, handler } objects
+ */
+export const disposeBreakpointListeners = listeners => {
+ for (const { mql, handler } of listeners) {
+ mql.removeEventListener('change', handler)
+ }
+}
const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20])
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdown = new Dropdown(btnDropdown, {
- offset: getOffset,
- popperConfig: {
- onFirstUpdate(state) {
- expect(getOffset).toHaveBeenCalledWith({
- popper: state.rects.popper,
- reference: state.rects.reference,
- placement: state.placement
- }, btnDropdown)
- resolve()
- }
- }
+ offset: getOffset
})
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ // Floating UI calls offset function asynchronously
+ setTimeout(() => {
+ expect(getOffset).toHaveBeenCalled()
+ resolve()
+ }, 20)
+ })
+
const offset = dropdown._getOffset()
expect(typeof offset).toEqual('function')
expect(dropdown._getOffset()).toEqual([10, 20])
})
- it('should allow to pass config to Popper with `popperConfig`', () => {
+ it('should allow to pass config to Floating UI with `floatingConfig`', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdown = new Dropdown(btnDropdown, {
- popperConfig: {
+ floatingConfig: {
placement: 'left'
}
})
- const popperConfig = dropdown._getPopperConfig()
+ const floatingConfig = dropdown._getFloatingConfig('bottom-start', [])
- expect(popperConfig.placement).toEqual('left')
+ expect(floatingConfig.placement).toEqual('left')
})
- it('should allow to pass config to Popper with `popperConfig` as a function', () => {
+ it('should allow to pass config to Floating UI with `floatingConfig` as a function', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right">Dropdown</button>',
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' })
+ const getFloatingConfig = jasmine.createSpy('getFloatingConfig').and.returnValue({ placement: 'left' })
const dropdown = new Dropdown(btnDropdown, {
- popperConfig: getPopperConfig
+ floatingConfig: getFloatingConfig
})
- const popperConfig = dropdown._getPopperConfig()
+ const floatingConfig = dropdown._getFloatingConfig('bottom-start', [])
// Ensure that the function was called with the default config.
- expect(getPopperConfig).toHaveBeenCalledWith(jasmine.objectContaining({
+ expect(getFloatingConfig).toHaveBeenCalledWith(jasmine.objectContaining({
placement: jasmine.any(String)
}))
- expect(popperConfig.placement).toEqual('left')
+ expect(floatingConfig.placement).toEqual('left')
})
})
})
})
- it('should destroy old popper references on toggle', () => {
+ it('should destroy old Floating UI references on toggle', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="first dropdown">',
firstDropdownEl.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown1).toHaveClass('show')
- spyOn(dropdown1._popper, 'destroy')
+ expect(dropdown1._floatingCleanup).not.toBeNull()
btnDropdown2.click()
})
secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
- expect(dropdown1._popper.destroy).toHaveBeenCalled()
+ expect(dropdown1._floatingCleanup).toBeNull()
resolve()
}))
}
})).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.')
- // use onFirstUpdate as Poppers internal update is executed async
const dropdown = new Dropdown(btnDropdown, {
- reference: virtualElement,
- popperConfig: {
- onFirstUpdate() {
- expect(spy).toHaveBeenCalled()
- expect(btnDropdown).toHaveClass('show')
- expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
- resolve()
- }
- }
+ reference: virtualElement
})
const spy = spyOn(virtualElement, 'getBoundingClientRect').and.callThrough()
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ // Floating UI calls getBoundingClientRect asynchronously
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled()
+ expect(btnDropdown).toHaveClass('show')
+ expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
+ resolve()
+ }, 20)
+ })
+
dropdown.toggle()
})
})
})
})
- it('should hide a dropdown and destroy popper', () => {
+ it('should hide a dropdown and cleanup Floating UI', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
- spyOn(dropdown._popper, 'destroy')
+ expect(dropdown._floatingCleanup).not.toBeNull()
dropdown.hide()
})
btnDropdown.addEventListener('hidden.bs.dropdown', () => {
- expect(dropdown._popper.destroy).toHaveBeenCalled()
+ expect(dropdown._floatingCleanup).toBeNull()
resolve()
})
const dropdown = new Dropdown(btnDropdown)
- expect(dropdown._popper).toBeNull()
+ expect(dropdown._floatingCleanup).toBeNull()
expect(dropdown._menu).not.toBeNull()
expect(dropdown._element).not.toBeNull()
const spy = spyOn(EventHandler, 'off')
expect(spy).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY)
})
- it('should dispose dropdown with Popper', () => {
+ it('should dispose dropdown with Floating UI', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
dropdown.toggle()
- expect(dropdown._popper).not.toBeNull()
+ expect(dropdown._floatingCleanup).not.toBeNull()
expect(dropdown._menu).not.toBeNull()
expect(dropdown._element).not.toBeNull()
dropdown.dispose()
- expect(dropdown._popper).toBeNull()
+ expect(dropdown._floatingCleanup).toBeNull()
expect(dropdown._menu).toBeNull()
expect(dropdown._element).toBeNull()
})
})
describe('update', () => {
- it('should call Popper and detect navbar on update', () => {
- fixtureEl.innerHTML = [
- '<div class="dropdown">',
- ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
- ' <div class="dropdown-menu">',
- ' <a class="dropdown-item" href="#">Secondary link</a>',
- ' </div>',
- '</div>'
- ].join('')
+ it('should call Floating UI update on update', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
- const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = new Dropdown(btnDropdown)
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdown = new Dropdown(btnDropdown)
- dropdown.toggle()
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdown._floatingCleanup).not.toBeNull()
- expect(dropdown._popper).not.toBeNull()
+ const spyUpdate = spyOn(dropdown, '_updateFloatingPosition')
- const spyUpdate = spyOn(dropdown._popper, 'update')
- const spyDetect = spyOn(dropdown, '_detectNavbar')
+ dropdown.update()
- dropdown.update()
+ expect(spyUpdate).toHaveBeenCalled()
+ resolve()
+ })
- expect(spyUpdate).toHaveBeenCalled()
- expect(spyDetect).toHaveBeenCalled()
+ dropdown.toggle()
+ })
})
- it('should just detect navbar on update', () => {
+ it('should do nothing on update if not shown', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdown = new Dropdown(btnDropdown)
- const spy = spyOn(dropdown, '_detectNavbar')
+ const spy = spyOn(dropdown, '_updateFloatingPosition')
dropdown.update()
- expect(dropdown._popper).toBeNull()
- expect(spy).toHaveBeenCalled()
+ expect(dropdown._floatingCleanup).toBeNull()
+ expect(spy).not.toHaveBeenCalled()
})
})
})
})
- it('should not use "static" Popper in navbar', () => {
+ it('should use Floating UI positioning in navbar', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<nav class="navbar navbar-expand-md bg-light">',
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
- expect(dropdown._popper).not.toBeNull()
- expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static')
- resolve()
+ expect(dropdown._floatingCleanup).not.toBeNull()
+ // Floating UI sets data-bs-placement attribute asynchronously
+ setTimeout(() => {
+ expect(dropdownMenu.getAttribute('data-bs-placement')).not.toBeNull()
+ resolve()
+ }, 10)
})
dropdown.show()
})
})
- it('should manage bs attribute `data-bs-popper`="static" when dropdown is in navbar', () => {
- return new Promise(resolve => {
- fixtureEl.innerHTML = [
- '<nav class="navbar navbar-expand-md bg-light">',
- ' <div class="dropdown">',
- ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
- ' <div class="dropdown-menu">',
- ' <a class="dropdown-item" href="#">Secondary link</a>',
- ' </div>',
- ' </div>',
- '</nav>'
- ].join('')
-
- const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
- const dropdown = new Dropdown(btnDropdown)
-
- btnDropdown.addEventListener('shown.bs.dropdown', () => {
- expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static')
- dropdown.hide()
- })
-
- btnDropdown.addEventListener('hidden.bs.dropdown', () => {
- expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
- resolve()
- })
-
- dropdown.show()
- })
- })
-
- it('should not use Popper if display set to static', () => {
+ it('should not use Floating UI if display set to static', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
btnDropdown.addEventListener('shown.bs.dropdown', () => {
- // Popper adds this attribute when we use it
- expect(dropdownMenu.getAttribute('data-popper-placement')).toBeNull()
+ // Floating UI adds this attribute when we use it
+ expect(dropdownMenu.getAttribute('data-bs-placement')).toBeNull()
resolve()
})
})
})
- it('should manage bs attribute `data-bs-popper`="static" when display set to static', () => {
+ it('should manage bs attribute `data-bs-display`="static" when display set to static', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
- expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static')
+ expect(dropdownMenu.getAttribute('data-bs-display')).toEqual('static')
dropdown.hide()
})
btnDropdown.addEventListener('hidden.bs.dropdown', () => {
- expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
+ expect(dropdownMenu.getAttribute('data-bs-display')).toBeNull()
resolve()
})
childElement.click()
})
})
+
+ describe('responsive placements', () => {
+ it('should parse responsive placement string and create instance', () => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="bottom-start md:top-end">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdown = new Dropdown(btnDropdown)
+
+ // Dropdown should have parsed responsive placements
+ expect(dropdown._responsivePlacements).not.toBeNull()
+ expect(dropdown._responsivePlacements.xs).toEqual('bottom-start')
+ expect(dropdown._responsivePlacements.md).toEqual('top-end')
+ })
+
+ it('should return null for non-responsive placement', () => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="bottom-start">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdown = new Dropdown(btnDropdown)
+
+ // Non-responsive placement should not create responsive placements object
+ expect(dropdown._responsivePlacements).toBeNull()
+ })
+ })
+
+ describe('virtual element reference', () => {
+ it('should work with virtual element as reference', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+
+ // Virtual element with getBoundingClientRect
+ const virtualElement = {
+ getBoundingClientRect() {
+ return {
+ width: 100,
+ height: 50,
+ top: 100,
+ left: 100,
+ right: 200,
+ bottom: 150,
+ x: 100,
+ y: 100
+ }
+ }
+ }
+
+ const dropdown = new Dropdown(btnDropdown, {
+ reference: virtualElement
+ })
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(document.querySelector('.dropdown-menu.show')).not.toBeNull()
+ resolve()
+ })
+
+ dropdown.show()
+ })
+ })
+
+ it('should throw error for object reference without getBoundingClientRect', () => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+
+ expect(() => {
+ // eslint-disable-next-line no-new
+ new Dropdown(btnDropdown, {
+ reference: { someProperty: 'value' } // Object without getBoundingClientRect
+ })
+ }).toThrowError(TypeError)
+ })
+ })
+
+ describe('selectMenuItem', () => {
+ it('should do nothing when dropdown menu has no visible items', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdown = new Dropdown(btnDropdown)
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ // Simulate ArrowDown key - should not throw when no items
+ const keydown = createEvent('keydown')
+ keydown.key = 'ArrowDown'
+ btnDropdown.dispatchEvent(keydown)
+
+ // No error thrown means test passed
+ expect(true).toBeTrue()
+ resolve()
+ })
+
+ dropdown.show()
+ })
+ })
+ })
})
expect(popover2._config.placement).toEqual('top')
})
})
+
+ describe('data-api', () => {
+ it('should toggle popover on click via data-api', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<a href="#" data-bs-toggle="popover" title="Popover Title" data-bs-content="Popover content">Click me</a>'
+
+ const popoverEl = fixtureEl.querySelector('[data-bs-toggle="popover"]')
+
+ popoverEl.addEventListener('shown.bs.popover', () => {
+ expect(document.querySelector('.popover')).not.toBeNull()
+ resolve()
+ })
+
+ popoverEl.click()
+ })
+ })
+
+ it('should do nothing when clicking on element without data-bs-toggle', () => {
+ fixtureEl.innerHTML = '<a href="#" title="Not a popover">Click me</a>'
+
+ const linkEl = fixtureEl.querySelector('a')
+ linkEl.click()
+
+ expect(document.querySelector('.popover')).toBeNull()
+ expect(Popover.getInstance(linkEl)).toBeNull()
+ })
+
+ it('should show popover on focusin via data-api', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<button data-bs-toggle="popover" data-bs-trigger="focus" title="Popover Title" data-bs-content="Popover content">Focus me</button>'
+
+ const popoverEl = fixtureEl.querySelector('[data-bs-toggle="popover"]')
+
+ popoverEl.addEventListener('shown.bs.popover', () => {
+ expect(document.querySelector('.popover')).not.toBeNull()
+ resolve()
+ })
+
+ const focusEvent = createEvent('focusin')
+ popoverEl.dispatchEvent(focusEvent)
+ })
+ })
+
+ it('should prevent default on click via data-api', () => {
+ fixtureEl.innerHTML = '<a href="#test" data-bs-toggle="popover" title="Popover Title" data-bs-content="Popover content">Click me</a>'
+
+ const popoverEl = fixtureEl.querySelector('[data-bs-toggle="popover"]')
+ const clickEvent = createEvent('click')
+ const preventDefaultSpy = spyOn(clickEvent, 'preventDefault').and.callThrough()
+
+ popoverEl.dispatchEvent(clickEvent)
+
+ expect(preventDefaultSpy).toHaveBeenCalled()
+ })
+ })
})
const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20])
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl, {
- offset: getOffset,
- popperConfig: {
- onFirstUpdate(state) {
- expect(getOffset).toHaveBeenCalledWith({
- popper: state.rects.popper,
- reference: state.rects.reference,
- placement: state.placement
- }, tooltipEl)
- resolve()
- }
- }
+ offset: getOffset
})
const offset = tooltip._getOffset()
expect(offset).toEqual(jasmine.any(Function))
+ tooltipEl.addEventListener('shown.bs.tooltip', () => {
+ // The offset function should have been called during positioning
+ expect(getOffset).toHaveBeenCalled()
+ resolve()
+ })
+
tooltip.show()
})
})
expect(tooltip._getOffset()).toEqual([10, 20])
})
- it('should allow to pass config to Popper with `popperConfig`', () => {
+ it('should allow to pass config to Floating UI with `floatingConfig`', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip"></a>'
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl, {
- popperConfig: {
+ floatingConfig: {
placement: 'left'
}
})
- const popperConfig = tooltip._getPopperConfig('top')
+ const floatingConfig = tooltip._getFloatingConfig('top', [])
- expect(popperConfig.placement).toEqual('left')
+ expect(floatingConfig.placement).toEqual('left')
})
- it('should allow to pass config to Popper with `popperConfig` as a function', () => {
+ it('should allow to pass config to Floating UI with `floatingConfig` as a function', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip"></a>'
const tooltipEl = fixtureEl.querySelector('a')
- const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' })
+ const getFloatingConfig = jasmine.createSpy('getFloatingConfig').and.returnValue({ placement: 'left' })
const tooltip = new Tooltip(tooltipEl, {
- popperConfig: getPopperConfig
+ floatingConfig: getFloatingConfig
})
- const popperConfig = tooltip._getPopperConfig('top')
+ const floatingConfig = tooltip._getFloatingConfig('top', [])
// Ensure that the function was called with the default config.
- expect(getPopperConfig).toHaveBeenCalledWith(jasmine.objectContaining({
+ expect(getFloatingConfig).toHaveBeenCalledWith(jasmine.objectContaining({
placement: jasmine.any(String)
}))
- expect(popperConfig.placement).toEqual('left')
+ expect(floatingConfig.placement).toEqual('left')
})
it('should use original title, if not "data-bs-title" is given', () => {
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl, {
- placement: 'bottom'
+ placement: 'bottom',
+ fallbackPlacements: [] // Disable flip to get exact placement
})
tooltipEl.addEventListener('inserted.bs.tooltip', () => {
tooltipEl.addEventListener('shown.bs.tooltip', () => {
expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto')
- expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('bottom')
+ expect(tooltip._getTipElement().getAttribute('data-bs-placement')).toEqual('bottom')
resolve()
})
})
})
- it('should throw an error the element is not visible', () => {
+ it('should throw an error the element is not visible', async () => {
fixtureEl.innerHTML = '<a href="#" style="display: none" rel="tooltip" title="Another tooltip"></a>'
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
try {
- tooltip.show()
+ await tooltip.show()
} catch (error) {
expect(error.message).toEqual('Please use show on visible elements')
}
it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', () => {
return new Promise(resolve => {
- // Style this tooltip to give it plenty of room for popper to do what it wants
+ // Style this tooltip to give it plenty of room for Floating UI to do what it wants
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-placement="top" style="position:fixed;left:50%;top:50%;">Trigger</a>'
const tooltipEl = fixtureEl.querySelector('a')
})
setTimeout(() => {
- expect(tooltip._popper).not.toBeNull()
- expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top')
+ expect(tooltip._floatingCleanup).not.toBeNull()
+ expect(tooltip._getTipElement().getAttribute('data-bs-placement')).toEqual('top')
tooltipEl.dispatchEvent(createEvent('mouseout'))
setTimeout(() => {
}, 100)
setTimeout(() => {
- expect(tooltip._popper).not.toBeNull()
- expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top')
+ expect(tooltip._floatingCleanup).not.toBeNull()
+ expect(tooltip._getTipElement().getAttribute('data-bs-placement')).toEqual('top')
resolve()
}, 200)
}, 10)
})
})
- it('should not throw error running hide if popper hasn\'t been shown', () => {
+ it('should not throw error running hide if tooltip hasn\'t been shown', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
})
describe('update', () => {
- it('should call popper update', () => {
+ it('should call floating position update', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'
const tooltip = new Tooltip(tooltipEl)
tooltipEl.addEventListener('shown.bs.tooltip', () => {
- const spy = spyOn(tooltip._popper, 'update')
+ const spy = spyOn(tooltip, '_updateFloatingPosition')
tooltip.update()
})
it('should re-show tip if it was already shown', () => {
- fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip"></a>'
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip"></a>'
- const tooltipEl = fixtureEl.querySelector('a')
- const tooltip = new Tooltip(tooltipEl)
- tooltip.show()
- const tip = () => tooltip._getTipElement()
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+ const tip = () => tooltip._getTipElement()
- expect(tip()).toHaveClass('show')
- tooltip.setContent({ '.tooltip-inner': 'foo' })
+ tooltipEl.addEventListener('shown.bs.tooltip', function handler() {
+ tooltipEl.removeEventListener('shown.bs.tooltip', handler)
- expect(tip()).toHaveClass('show')
- expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo')
+ expect(tip()).toHaveClass('show')
+
+ // Listen for the re-show after setContent
+ tooltipEl.addEventListener('shown.bs.tooltip', () => {
+ expect(tip()).toHaveClass('show')
+ expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo')
+ resolve()
+ })
+
+ tooltip.setContent({ '.tooltip-inner': 'foo' })
+ })
+
+ tooltip.show()
+ })
})
it('should keep tip hidden, if it was already hidden before', () => {
--- /dev/null
+import {
+ BREAKPOINTS,
+ getDefaultPlacement,
+ parseResponsivePlacement,
+ getResponsivePlacement,
+ createBreakpointListeners,
+ disposeBreakpointListeners
+} from '../../../src/util/floating-ui.js'
+
+describe('FloatingUI Util', () => {
+ describe('BREAKPOINTS', () => {
+ it('should export breakpoint values', () => {
+ expect(BREAKPOINTS).toEqual(jasmine.any(Object))
+ expect(BREAKPOINTS.sm).toBe(576)
+ expect(BREAKPOINTS.md).toBe(768)
+ expect(BREAKPOINTS.lg).toBe(1024)
+ expect(BREAKPOINTS.xl).toBe(1280)
+ expect(BREAKPOINTS['2xl']).toBe(1536)
+ })
+ })
+
+ describe('getDefaultPlacement', () => {
+ it('should return fallback placement when no alignment', () => {
+ expect(getDefaultPlacement('bottom')).toBe('bottom')
+ expect(getDefaultPlacement('top')).toBe('top')
+ expect(getDefaultPlacement('left')).toBe('left')
+ expect(getDefaultPlacement('right')).toBe('right')
+ })
+
+ it('should return default "bottom" when no argument provided', () => {
+ expect(getDefaultPlacement()).toBe('bottom')
+ })
+
+ it('should return placement with alignment in LTR mode', () => {
+ // In LTR mode (default), alignment should be preserved
+ const htmlEl = document.documentElement
+ const originalDir = htmlEl.getAttribute('dir')
+ htmlEl.removeAttribute('dir')
+
+ expect(getDefaultPlacement('bottom-start')).toBe('bottom-start')
+ expect(getDefaultPlacement('bottom-end')).toBe('bottom-end')
+ expect(getDefaultPlacement('top-start')).toBe('top-start')
+ expect(getDefaultPlacement('top-end')).toBe('top-end')
+
+ if (originalDir) {
+ htmlEl.setAttribute('dir', originalDir)
+ }
+ })
+
+ it('should flip alignment in RTL mode', () => {
+ const htmlEl = document.documentElement
+ const originalDir = htmlEl.getAttribute('dir')
+ htmlEl.setAttribute('dir', 'rtl')
+
+ expect(getDefaultPlacement('bottom-start')).toBe('bottom-end')
+ expect(getDefaultPlacement('bottom-end')).toBe('bottom-start')
+ expect(getDefaultPlacement('top-start')).toBe('top-end')
+ expect(getDefaultPlacement('top-end')).toBe('top-start')
+
+ if (originalDir) {
+ htmlEl.setAttribute('dir', originalDir)
+ } else {
+ htmlEl.removeAttribute('dir')
+ }
+ })
+ })
+
+ describe('parseResponsivePlacement', () => {
+ it('should return null for non-responsive placement strings', () => {
+ expect(parseResponsivePlacement('bottom')).toBeNull()
+ expect(parseResponsivePlacement('top-start')).toBeNull()
+ expect(parseResponsivePlacement('left-end')).toBeNull()
+ expect(parseResponsivePlacement('')).toBeNull()
+ expect(parseResponsivePlacement(null)).toBeNull()
+ expect(parseResponsivePlacement(undefined)).toBeNull()
+ })
+
+ it('should parse simple responsive placement', () => {
+ const result = parseResponsivePlacement('bottom md:top')
+ expect(result).toEqual({
+ xs: 'bottom',
+ md: 'top'
+ })
+ })
+
+ it('should parse responsive placement with alignments', () => {
+ const result = parseResponsivePlacement('bottom-start md:top-end lg:right')
+ expect(result).toEqual({
+ xs: 'bottom-start',
+ md: 'top-end',
+ lg: 'right'
+ })
+ })
+
+ it('should parse all breakpoints', () => {
+ const result = parseResponsivePlacement('bottom sm:top md:left lg:right xl:bottom-start 2xl:top-end')
+ expect(result).toEqual({
+ xs: 'bottom',
+ sm: 'top',
+ md: 'left',
+ lg: 'right',
+ xl: 'bottom-start',
+ '2xl': 'top-end'
+ })
+ })
+
+ it('should use default placement for xs when base is not specified', () => {
+ const result = parseResponsivePlacement('md:top lg:bottom', 'right')
+ expect(result).toEqual({
+ xs: 'right',
+ md: 'top',
+ lg: 'bottom'
+ })
+ })
+
+ it('should ignore invalid breakpoints', () => {
+ const result = parseResponsivePlacement('bottom invalid:top md:left')
+ expect(result).toEqual({
+ xs: 'bottom',
+ md: 'left'
+ })
+ expect(result.invalid).toBeUndefined()
+ })
+
+ it('should handle placement string with only responsive prefixes', () => {
+ const result = parseResponsivePlacement('md:top')
+ expect(result).toEqual({
+ xs: 'bottom', // default
+ md: 'top'
+ })
+ })
+ })
+
+ describe('getResponsivePlacement', () => {
+ it('should return default placement when responsivePlacements is null', () => {
+ expect(getResponsivePlacement(null)).toBe('bottom')
+ expect(getResponsivePlacement(null, 'top')).toBe('top')
+ })
+
+ it('should return default placement when responsivePlacements is undefined', () => {
+ expect(getResponsivePlacement(undefined)).toBe('bottom')
+ expect(getResponsivePlacement(undefined, 'left')).toBe('left')
+ })
+
+ it('should return xs placement for small viewports', () => {
+ // Mock a small viewport (less than sm breakpoint)
+ spyOnProperty(window, 'innerWidth').and.returnValue(400)
+
+ const placements = { xs: 'bottom', md: 'top' }
+ expect(getResponsivePlacement(placements)).toBe('bottom')
+ })
+
+ it('should return appropriate placement for sm viewport', () => {
+ spyOnProperty(window, 'innerWidth').and.returnValue(600)
+
+ const placements = { xs: 'bottom', sm: 'top', md: 'left' }
+ expect(getResponsivePlacement(placements)).toBe('top')
+ })
+
+ it('should return appropriate placement for md viewport', () => {
+ spyOnProperty(window, 'innerWidth').and.returnValue(800)
+
+ const placements = {
+ xs: 'bottom',
+ sm: 'top',
+ md: 'left',
+ lg: 'right'
+ }
+ expect(getResponsivePlacement(placements)).toBe('left')
+ })
+
+ it('should return appropriate placement for lg viewport', () => {
+ spyOnProperty(window, 'innerWidth').and.returnValue(1100)
+
+ const placements = { xs: 'bottom', md: 'top', lg: 'right' }
+ expect(getResponsivePlacement(placements)).toBe('right')
+ })
+
+ it('should return appropriate placement for xl viewport', () => {
+ spyOnProperty(window, 'innerWidth').and.returnValue(1300)
+
+ const placements = { xs: 'bottom', lg: 'top', xl: 'left' }
+ expect(getResponsivePlacement(placements)).toBe('left')
+ })
+
+ it('should return appropriate placement for 2xl viewport', () => {
+ spyOnProperty(window, 'innerWidth').and.returnValue(1600)
+
+ const placements = { xs: 'bottom', xl: 'top', '2xl': 'right-start' }
+ expect(getResponsivePlacement(placements)).toBe('right-start')
+ })
+
+ it('should cascade to smaller breakpoints when larger ones are not defined', () => {
+ spyOnProperty(window, 'innerWidth').and.returnValue(1600)
+
+ // Only xs and md defined, viewport is 2xl
+ const placements = { xs: 'bottom', md: 'top' }
+ expect(getResponsivePlacement(placements)).toBe('top')
+ })
+
+ it('should use default when xs is not defined', () => {
+ spyOnProperty(window, 'innerWidth').and.returnValue(400)
+
+ const placements = { md: 'top' } // No xs
+ expect(getResponsivePlacement(placements, 'left')).toBe('left')
+ })
+ })
+
+ describe('createBreakpointListeners', () => {
+ it('should create listeners for all breakpoints', () => {
+ const callback = jasmine.createSpy('callback')
+ const listeners = createBreakpointListeners(callback)
+
+ expect(listeners).toEqual(jasmine.any(Array))
+ expect(listeners.length).toBe(Object.keys(BREAKPOINTS).length)
+
+ // Each listener should have mql and handler
+ for (const listener of listeners) {
+ expect(listener.mql).toBeDefined()
+ expect(listener.handler).toBe(callback)
+ }
+
+ // Clean up
+ disposeBreakpointListeners(listeners)
+ })
+
+ it('should create MediaQueryList objects with correct queries', () => {
+ const callback = jasmine.createSpy('callback')
+ const listeners = createBreakpointListeners(callback)
+
+ // Verify media query objects are created
+ expect(listeners[0].mql.media).toContain('min-width')
+
+ // Clean up
+ disposeBreakpointListeners(listeners)
+ })
+ })
+
+ describe('disposeBreakpointListeners', () => {
+ it('should remove all event listeners', () => {
+ const callback = jasmine.createSpy('callback')
+ const listeners = createBreakpointListeners(callback)
+
+ // Spy on removeEventListener for each mql
+ const spies = listeners.map(listener =>
+ spyOn(listener.mql, 'removeEventListener').and.callThrough()
+ )
+
+ disposeBreakpointListeners(listeners)
+
+ // Verify removeEventListener was called on each
+ for (const spy of spies) {
+ expect(spy).toHaveBeenCalledWith('change', callback)
+ }
+ })
+
+ it('should handle empty array', () => {
+ expect(() => disposeBreakpointListeners([])).not.toThrow()
+ })
+ })
+})
<div class="col-sm-3 mt-4">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
- Dropdown menu without Popper
+ Dropdown menu with static display
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@docsearch/js": "^3.9.0",
- "@popperjs/core": "^2.11.8",
+ "@floating-ui/dom": "^1.7.0",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"zod": "^4.1.12"
},
"peerDependencies": {
- "@popperjs/core": "^2.11.8"
+ "@floating-ui/dom": "^1.7.0"
}
},
"node_modules/@algolia/abtesting": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@html-validate/stylish": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@html-validate/stylish/-/stylish-4.3.0.tgz",
"node": ">=14"
}
},
- "node_modules/@popperjs/core": {
- "version": "2.11.8",
- "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
- "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/popperjs"
- }
- },
"node_modules/@prettier/sync": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@prettier/sync/-/sync-0.6.1.tgz",
"astro-preview": "astro preview --root site --port 9001"
},
"peerDependencies": {
- "@popperjs/core": "^2.11.8"
+ "@floating-ui/dom": "^1.7.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@docsearch/js": "^3.9.0",
- "@popperjs/core": "^2.11.8",
+ "@floating-ui/dom": "^1.7.0",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"shim": {
"js/bootstrap": {
"deps": [
- "@popperjs/core"
+ "@floating-ui/dom"
]
}
},
"dependencies": {},
"peerDependencies": {
- "@popperjs/core": "^2.11.8"
+ "@floating-ui/dom": "^1.7.0"
}
}
}
@use "config" as *;
@use "colors" as *;
@use "variables" as *;
-@use "mixins/caret" as *;
@use "mixins/border-radius" as *;
@use "mixins/box-shadow" as *;
@use "mixins/gradients" as *;
-@use "mixins/caret" as *;
-@use "layout/breakpoints" as *;
// scss-docs-start dropdown-variables
+$dropdown-gap: $spacer * .125 !default;
$dropdown-min-width: 10rem !default;
-$dropdown-padding-x: 0 !default;
-$dropdown-padding-y: .5rem !default;
+$dropdown-padding-x: .25rem !default;
+$dropdown-padding-y: .25rem !default;
$dropdown-spacer: .125rem !default;
$dropdown-font-size: $font-size-base !default;
-$dropdown-color: var(--#{$prefix}color-body) !default;
+$dropdown-color: var(--#{$prefix}fg-body) !default;
$dropdown-bg: var(--#{$prefix}bg-body) !default;
$dropdown-border-color: var(--#{$prefix}border-color-translucent) !default;
-$dropdown-border-radius: var(--#{$prefix}border-radius) !default;
+$dropdown-border-radius: var(--#{$prefix}border-radius-lg) !default;
$dropdown-border-width: var(--#{$prefix}border-width) !default;
$dropdown-inner-border-radius: calc(#{$dropdown-border-radius} - #{$dropdown-border-width}) !default;
-$dropdown-divider-bg: $dropdown-border-color !default;
-$dropdown-divider-margin-y: $spacer * .5 !default;
$dropdown-box-shadow: var(--#{$prefix}box-shadow) !default;
-$dropdown-link-color: var(--#{$prefix}color-body) !default;
+$dropdown-divider-bg: $dropdown-border-color !default;
+$dropdown-divider-margin-y: $spacer * .125 !default;
+$dropdown-divider-margin-x: $spacer * .25 !default;
+
+$dropdown-link-color: var(--#{$prefix}fg-body) !default;
$dropdown-link-hover-color: $dropdown-link-color !default;
-$dropdown-link-hover-bg: var(--#{$prefix}tertiary-bg) !default;
+$dropdown-link-hover-bg: var(--#{$prefix}bg-1) !default;
$dropdown-link-active-color: $component-active-color !default;
$dropdown-link-active-bg: $component-active-bg !default;
-$dropdown-link-disabled-color: var(--#{$prefix}tertiary-color) !default;
+$dropdown-link-disabled-color: var(--#{$prefix}fg-3) !default;
$dropdown-item-padding-y: $spacer * .25 !default;
-$dropdown-item-padding-x: $spacer !default;
+$dropdown-item-padding-x: $spacer * .75 !default;
+$dropdown-item-border-radius: var(--#{$prefix}border-radius) !default;
+$dropdown-item-gap: $spacer * .5 !default;
$dropdown-header-color: var(--#{$prefix}gray-600) !default;
$dropdown-header-padding-x: $dropdown-item-padding-x !default;
// scss-docs-start dropdown-dark-variables
$dropdown-dark-color: var(--#{$prefix}gray-300) !default;
-$dropdown-dark-bg: var(--#{$prefix}gray-800) !default;
+$dropdown-dark-bg: var(--#{$prefix}gray-900) !default;
$dropdown-dark-border-color: $dropdown-border-color !default;
$dropdown-dark-divider-bg: $dropdown-divider-bg !default;
$dropdown-dark-box-shadow: null !default;
@layer components {
// The dropdown wrapper (`<div>`)
- .dropup,
- .dropend,
- .dropdown,
- .dropstart,
- .dropup-center,
- .dropdown-center {
+ .dropdown {
position: relative;
}
- .dropdown-toggle {
- white-space: nowrap;
-
- // Generate the caret automatically
- @include caret();
- }
-
// The dropdown menu
.dropdown-menu {
// scss-docs-start dropdown-css-vars
--#{$prefix}dropdown-zindex: #{$zindex-dropdown};
+ --#{$prefix}dropdown-gap: #{$dropdown-gap};
--#{$prefix}dropdown-min-width: #{$dropdown-min-width};
--#{$prefix}dropdown-padding-x: #{$dropdown-padding-x};
--#{$prefix}dropdown-padding-y: #{$dropdown-padding-y};
--#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius};
--#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg};
--#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y};
+ --#{$prefix}dropdown-divider-margin-x: #{$dropdown-divider-margin-x};
--#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow};
--#{$prefix}dropdown-link-color: #{$dropdown-link-color};
--#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color};
--#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color};
--#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg};
--#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color};
+ --#{$prefix}dropdown-item-gap: #{$dropdown-item-gap};
--#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x};
--#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y};
+ --#{$prefix}dropdown-item-border-radius: #{$dropdown-item-border-radius};
--#{$prefix}dropdown-header-color: #{$dropdown-header-color};
--#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x};
--#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y};
position: absolute;
z-index: var(--#{$prefix}dropdown-zindex);
- display: none; // none by default, but block on "open" of the menu
+ display: none; // none by default, but flex on "open" of the menu
+ flex-direction: column;
+ gap: var(--#{$prefix}dropdown-gap);
min-width: var(--#{$prefix}dropdown-min-width);
padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x);
margin: 0; // Override default margin of ul
@include border-radius(var(--#{$prefix}dropdown-border-radius));
@include box-shadow(var(--#{$prefix}dropdown-box-shadow));
- &[data-bs-popper] {
- top: 100%;
- left: 0;
- margin-top: var(--#{$prefix}dropdown-spacer);
- }
-
@if $dropdown-padding-y == 0 {
> .dropdown-item:first-child,
> li:first-child .dropdown-item {
> li:last-child .dropdown-item {
@include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius));
}
-
- }
- }
-
- // scss-docs-start responsive-breakpoints
- // We deliberately hardcode the `bs-` prefix because we check
- // this custom property in JS to determine Popper's positioning
-
- @each $breakpoint in map.keys($grid-breakpoints) {
- @include media-breakpoint-up($breakpoint) {
- $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
-
- .dropdown-menu#{$infix}-start {
- --bs-position: start;
-
- &[data-bs-popper] {
- right: auto;
- left: 0;
- }
- }
-
- .dropdown-menu#{$infix}-end {
- --bs-position: end;
-
- &[data-bs-popper] {
- right: 0;
- left: auto;
- }
- }
- }
- }
- // scss-docs-end responsive-breakpoints
-
- // Allow for dropdowns to go bottom up (aka, dropup-menu)
- // Just add .dropup after the standard .dropdown class and you're set.
- .dropup {
- .dropdown-menu[data-bs-popper] {
- top: auto;
- bottom: 100%;
- margin-top: 0;
- margin-bottom: var(--#{$prefix}dropdown-spacer);
- }
-
- .dropdown-toggle {
- @include caret(up);
- }
- }
-
- .dropend {
- .dropdown-menu[data-bs-popper] {
- top: 0;
- right: auto;
- left: 100%;
- margin-inline-start: var(--#{$prefix}dropdown-spacer);
- margin-top: 0;
- }
-
- .dropdown-toggle {
- @include caret(end);
- &::after {
- vertical-align: 0;
- }
}
}
- .dropstart {
- .dropdown-menu[data-bs-popper] {
- top: 0;
- right: 100%;
- left: auto;
- margin-inline-end: var(--#{$prefix}dropdown-spacer);
- margin-top: 0;
- }
-
- .dropdown-toggle {
- @include caret(start);
- &::before {
- vertical-align: 0;
- }
- }
+ .dropdown-menu.show {
+ display: flex;
}
-
// Dividers (basically an `<hr>`) within the dropdown
.dropdown-divider {
height: 0;
- margin: var(--#{$prefix}dropdown-divider-margin-y) 0;
+ margin: var(--#{$prefix}dropdown-divider-margin-y) var(--#{$prefix}dropdown-divider-margin-x);
overflow: hidden;
border-block-start: 1px solid var(--#{$prefix}dropdown-divider-bg);
- opacity: 1; // Revisit in v6 to de-dupe styles that conflict with <hr> element
+ opacity: 1;
}
// Links, buttons, and more within the dropdown menu
//
// `<button>`-specific styles are denoted with `// For <button>s`
.dropdown-item {
- display: block;
+ display: flex;
+ gap: var(--#{$prefix}dropdown-item-gap);
+ align-items: center;
width: 100%; // For `<button>`s
padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x);
- clear: both;
font-weight: $font-weight-normal;
color: var(--#{$prefix}dropdown-link-color);
text-align: inherit; // For `<button>`s
}
}
- .dropdown-menu.show {
- display: block;
- }
-
// Dropdown section headers
.dropdown-header {
display: block;
.dropdown-item-text {
display: block;
padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x);
- color: var(--#{$prefix}dropdown-link-color);
+ color: var(--#{$prefix}fg-2);
}
// Dark dropdowns
@use "mixins/reset-text" as *;
// scss-docs-start popover-variables
-$popover-font-size: $font-size-sm !default;
+$popover-font-size: var(--#{$prefix}font-size-sm) !default;
$popover-bg: var(--#{$prefix}bg-body) !default;
-$popover-max-width: 276px !default;
+$popover-max-width: 280px !default;
$popover-border-width: var(--#{$prefix}border-width) !default;
$popover-border-color: var(--#{$prefix}border-color-translucent) !default;
$popover-border-radius: var(--#{$prefix}border-radius-lg) !default;
$popover-box-shadow: var(--#{$prefix}box-shadow) !default;
$popover-header-font-size: $font-size-base !default;
-$popover-header-bg: var(--#{$prefix}secondary-bg) !default;
+$popover-header-bg: var(--#{$prefix}bg-1) !default;
$popover-header-color: $headings-color !default;
-$popover-header-padding-y: .5rem !default;
+$popover-header-padding-y: $spacer * .75 !default;
$popover-header-padding-x: $spacer !default;
$popover-body-color: var(--#{$prefix}color-body) !default;
-$popover-body-padding-y: $spacer !default;
+$popover-body-padding-y: $spacer * .75 !default;
$popover-body-padding-x: $spacer !default;
$popover-arrow-width: 1rem !default;
}
.bs-popover-auto {
- &[data-popper-placement^="top"] {
+ &[data-bs-placement^="top"] {
@extend .bs-popover-top;
}
- &[data-popper-placement^="right"] {
+ &[data-bs-placement^="right"] {
@extend .bs-popover-end;
}
- &[data-popper-placement^="bottom"] {
+ &[data-bs-placement^="bottom"] {
@extend .bs-popover-bottom;
}
- &[data-popper-placement^="left"] {
+ &[data-bs-placement^="left"] {
@extend .bs-popover-start;
}
}
@use "mixins/reset-text" as *;
// scss-docs-start tooltip-variables
-$tooltip-font-size: $font-size-sm !default;
+$tooltip-font-size: var(--#{$prefix}font-size-sm) !default;
$tooltip-max-width: 200px !default;
$tooltip-color: var(--#{$prefix}bg-body) !default;
-$tooltip-bg: var(--#{$prefix}color-body) !default;
+$tooltip-bg: var(--#{$prefix}fg-body) !default;
$tooltip-border-radius: var(--#{$prefix}border-radius) !default;
-$tooltip-opacity: .9 !default;
-$tooltip-padding-y: $spacer * .25 !default;
-$tooltip-padding-x: $spacer * .5 !default;
-$tooltip-margin: null !default; // TODO: remove this in v6
+$tooltip-opacity: .95 !default;
+$tooltip-padding-y: $spacer * .375 !default;
+$tooltip-padding-x: $spacer * .75 !default;
$tooltip-arrow-width: .8rem !default;
$tooltip-arrow-height: .4rem !default;
--#{$prefix}tooltip-max-width: #{$tooltip-max-width};
--#{$prefix}tooltip-padding-x: #{$tooltip-padding-x};
--#{$prefix}tooltip-padding-y: #{$tooltip-padding-y};
- --#{$prefix}tooltip-margin: #{$tooltip-margin};
--#{$prefix}tooltip-font-size: #{$tooltip-font-size};
--#{$prefix}tooltip-color: #{$tooltip-color};
--#{$prefix}tooltip-bg: #{$tooltip-bg};
z-index: var(--#{$prefix}tooltip-zindex);
display: block;
- margin: var(--#{$prefix}tooltip-margin);
- @include deprecate("`$tooltip-margin`", "v5", "v5.x", true);
// Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
// So reset our font and text properties to avoid inheriting weird values.
@include reset-text();
}
.bs-tooltip-auto {
- &[data-popper-placement^="top"] {
+ &[data-bs-placement^="top"] {
@extend .bs-tooltip-top;
}
- &[data-popper-placement^="right"] {
+ &[data-bs-placement^="right"] {
@extend .bs-tooltip-end;
}
- &[data-popper-placement^="bottom"] {
+ &[data-bs-placement^="bottom"] {
@extend .bs-tooltip-bottom;
}
- &[data-popper-placement^="left"] {
+ &[data-bs-placement^="left"] {
@extend .bs-tooltip-start;
}
}
margin-block: 0;
}
- li:not(:last-child) {
+ :where(ul, ol):not([class]) li:not(:last-child) {
margin-bottom: calc(var(--#{$prefix}content-gap) / 4);
}
- title: Offcanvas
- title: Pagination
- title: Placeholders
- - title: Popovers
+ - title: Popover
- title: Progress
- title: Scrollspy
- title: Spinners
- title: Toasts
- - title: Tooltips
+ - title: Tooltip
- title: Helpers
icon: magic
<article class="my-3" id="popovers">
<div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
<h3>الصناديق المنبثقة</h3>
- <a class="d-flex align-items-center" hreflang="en" href={getVersionedDocsPath('components/popovers')}>دليل الإستخدام</a>
+ <a class="d-flex align-items-center" hreflang="en" href={getVersionedDocsPath('components/popover')}>دليل الإستخدام</a>
</div>
<div>
<article class="mt-3 mb-5 pb-5" id="tooltips">
<div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
<h3>التلميحات</h3>
- <a class="d-flex align-items-center" hreflang="en" href={getVersionedDocsPath('components/tooltips')}>دليل الإستخدام</a>
+ <a class="d-flex align-items-center" hreflang="en" href={getVersionedDocsPath('components/tooltip')}>دليل الإستخدام</a>
</div>
<div>
<article class="mt-3 mb-5 pb-5" id="tooltips">
<div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
<h3>Tooltips</h3>
- <a class="d-flex align-items-center" href={getVersionedDocsPath('components/tooltips')}>Documentation</a>
+ <a class="d-flex align-items-center" href={getVersionedDocsPath('components/tooltip')}>Documentation</a>
</div>
<div>
---
-title: Dropdowns
-description: Toggle contextual overlays for displaying lists of links and more with the Bootstrap dropdown plugin.
+title: Dropdown
+description: Toggle contextual menus and custom overlays for lists of links, forms, and more with the Bootstrap dropdown plugin. Powered by [Floating UI](https://floating-ui.com/).
toc: true
---
## Overview
+Toggle dropdown menus with buttons whenever possible. Here's an example using a Bootstrap button:
+
+<Example code={`<div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+ Toggle dropdown
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Dropdown item 1</a></li>
+ <li><a class="dropdown-item" href="#">Dropdown item 2</a></li>
+ <li><a class="dropdown-item" href="#">Dropdown item 3</a></li>
+ </ul>
+ </div>`} />
+
Dropdowns are toggleable, contextual overlays for displaying lists of links and more. They’re made interactive with the included Bootstrap dropdown JavaScript plugin. They’re toggled by clicking, not by hovering; this is [an intentional design decision](https://markdotto.com/blog/bootstrap-explained-dropdowns/).
-Dropdowns are built on a third party library, [Popper](https://popper.js.org/docs/v2/), which provides dynamic positioning and viewport detection. Be sure to include [popper.min.js]([[config:cdn.popper]]) before Bootstrap’s JavaScript or use `bootstrap.bundle.min.js` / `bootstrap.bundle.js` which contains Popper. Popper isn’t used to position dropdowns in navbars though as dynamic positioning isn’t required.
+Dropdowns are built on a third party library, [Floating UI](https://floating-ui.com/), which provides dynamic positioning and viewport detection. Be sure to include [floating-ui.dom.umd.min.js]([[config:cdn.floating_ui]]) before Bootstrap’s JavaScript or use `bootstrap.bundle.min.js` / `bootstrap.bundle.js` which contains Floating UI. Popper isn’t used to position dropdowns in navbars though as dynamic positioning isn’t required.
## Accessibility
### Single button
-Any single `.btn` can be turned into a dropdown toggle with some markup changes. Here’s how you can put them to work with `<button>` elements:
-
-<Example code={`<div class="dropdown">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Dropdown button
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- </ul>
- </div>`} />
-
While `<button>` is the recommended control for a dropdown toggle, there might be situations where you have to use an `<a>` element. If you do, we recommend adding a `role="button"` attribute to appropriately convey control’s purpose to assistive technologies such as screen readers.
<Example code={`<div class="dropdown">
- <a class="btn btn-secondary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
- Dropdown link
+ <a class="btn btn-solid theme-secondary" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+ Toggle dropdown
</a>
<ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
+ <li><a class="dropdown-item" href="#">Dropdown item 1</a></li>
+ <li><a class="dropdown-item" href="#">Dropdown item 2</a></li>
+ <li><a class="dropdown-item" href="#">Dropdown item 3</a></li>
</ul>
</div>`} />
-The best part is you can do this with any button variant, too:
-
-<Example showMarkup={false} code={`<div class="btn-group">
- <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Primary</button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Secondary</button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Success</button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Info</button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-warning dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Warning</button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-danger dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Danger</button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>`} />
-
-```html
-<!-- Example single danger button -->
-<div class="btn-group">
- <button type="button" class="btn btn-danger dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Danger
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
-</div>
-```
-
### Split button
Similarly, create split button dropdowns with virtually the same markup as single button dropdowns, but with the addition of `.dropdown-toggle-split` for proper spacing around the dropdown caret.
We use this extra class to reduce the horizontal `padding` on either side of the caret by 25% and remove the `margin-left` that’s added for regular button dropdowns. Those extra changes keep the caret centered in the split button and provide a more appropriately sized hit area next to the main button.
<Example showMarkup={false} code={`<div class="btn-group">
- <button type="button" class="btn btn-primary">Primary</button>
- <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-secondary">Secondary</button>
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-success">Success</button>
- <button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-info">Info</button>
- <button type="button" class="btn btn-info dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-warning">Warning</button>
- <button type="button" class="btn btn-warning dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-danger">Danger</button>
- <button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
+ <button type="button" class="btn btn-solid theme-primary">Primary</button>
+ <button type="button" class="btn btn-solid theme-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
</div>
```
-## Sizing
-
-Button dropdowns work with buttons of all sizes, including default and split dropdown buttons.
-
-<Example showMarkup={false} code={`<div class="btn-group">
- <button class="btn btn-secondary btn-lg dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Large button
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-lg btn-secondary">Large split button</button>
- <button type="button" class="btn btn-lg btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>`} />
-
-```html
-<!-- Large button groups (default and split) -->
-<div class="btn-group">
- <button class="btn btn-secondary btn-lg dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Large button
- </button>
- <ul class="dropdown-menu">
- ...
- </ul>
-</div>
-<div class="btn-group">
- <button class="btn btn-secondary btn-lg" type="button">
- Large split button
- </button>
- <button type="button" class="btn btn-lg btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- ...
- </ul>
-</div>
-```
-
-<Example showMarkup={false} code={`<div class="btn-group">
- <button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Small button
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>
- <div class="btn-group">
- <button type="button" class="btn btn-sm btn-secondary">Small split button</button>
- <button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
- </ul>
- </div>`} />
-
-```html
-<div class="btn-group">
- <button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Small button
- </button>
- <ul class="dropdown-menu">
- ...
- </ul>
-</div>
-<div class="btn-group">
- <button class="btn btn-secondary btn-sm" type="button">
- Small split button
- </button>
- <button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- ...
- </ul>
-</div>
-```
-
## Dark dropdowns
Opt into darker dropdowns to match a dark navbar or custom style by adding `data-bs-theme="dark"` onto an existing `.dropdown-menu`. No changes are required to the dropdown items.
<Example code={`<div class="dropdown" data-bs-theme="dark">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown button
</button>
<ul class="dropdown-menu dropdown-menu-dark">
</div>
</nav>`} />
-## Directions
+## Placement
+
+Use `data-bs-placement` on the toggle element to control where the dropdown menu appears. Placement options include `top`, `bottom`, `left`, and `right`, each with optional `-start` and `-end` alignment modifiers.
<Callout>
-**Directions are flipped in RTL mode.** As such, `.dropstart` will appear on the right side.
+**Directions are flipped in RTL mode.** As such, `left` placements will appear on the right side.
</Callout>
-### Centered
+### Bottom
-Make the dropdown menu centered below the toggle with `.dropdown-center` on the parent element.
+The default placement. Use `bottom`, `bottom-start`, or `bottom-end` to position the menu below the toggle.
-<Example code={`<div class="dropdown-center">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Centered dropdown
+<Example class="d-flex gap-2" code={`<div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="bottom-start" aria-expanded="false">
+ Bottom start
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Action two</a></li>
- <li><a class="dropdown-item" href="#">Action three</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
+ </ul>
+ </div>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="bottom" aria-expanded="false">
+ Bottom
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Action</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
+ </ul>
+ </div>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="bottom-end" aria-expanded="false">
+ Bottom end
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Action</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
</ul>
</div>`} />
-### Dropup
+### Top
-Trigger dropdown menus above elements by adding `.dropup` to the parent element.
+Use `top`, `top-start`, or `top-end` to position the menu above the toggle.
-<Example showMarkup={false} code={`<div class="btn-group dropup">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropup
+<Example class="d-flex gap-2" code={`<div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="top-start" aria-expanded="false">
+ Top start
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
</div>
- <div class="btn-group dropup">
- <button type="button" class="btn btn-secondary">Split dropup</button>
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="top" aria-expanded="false">
+ Top
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Action</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
+ </ul>
+ </div>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="top-end" aria-expanded="false">
+ Top end
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
</div>`} />
-```html
-<!-- Default dropup button -->
-<div class="btn-group dropup">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropup
- </button>
- <ul class="dropdown-menu">
- <!-- Dropdown menu links -->
- </ul>
-</div>
+### Left
-<!-- Split dropup button -->
-<div class="btn-group dropup">
- <button type="button" class="btn btn-secondary">
- Split dropup
- </button>
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropdown</span>
- </button>
- <ul class="dropdown-menu">
- <!-- Dropdown menu links -->
- </ul>
-</div>
-```
-
-### Dropup centered
+Use `left`, `left-start`, or `left-end` to position the menu to the left of the toggle.
-Make the dropup menu centered above the toggle with `.dropup-center` on the parent element.
-
-<Example code={`<div class="dropup-center dropup">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Centered dropup
+<Example class="d-flex gap-2" code={`<div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="left-start" aria-expanded="false">
+ Left start
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Action</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
+ </ul>
+ </div>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="left" aria-expanded="false">
+ Left
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Action</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
+ </ul>
+ </div>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="left-end" aria-expanded="false">
+ Left end
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Action two</a></li>
- <li><a class="dropdown-item" href="#">Action three</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
</ul>
</div>`} />
-### Dropend
+### Right
-Trigger dropdown menus at the right of the elements by adding `.dropend` to the parent element.
+Use `right`, `right-start`, or `right-end` to position the menu to the right of the toggle.
-<Example showMarkup={false} code={`<div class="btn-group dropend">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropend
+<Example class="d-flex gap-2" code={`<div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="right-start" aria-expanded="false">
+ Right start
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
</div>
- <div class="btn-group dropend">
- <button type="button" class="btn btn-secondary">Split dropend</button>
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropend</span>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="right" aria-expanded="false">
+ Right
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Action</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
+ </ul>
+ </div>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="right-end" aria-expanded="false">
+ Right end
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
</div>`} />
-```html
-<!-- Default dropend button -->
-<div class="btn-group dropend">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropend
- </button>
- <ul class="dropdown-menu">
- <!-- Dropdown menu links -->
- </ul>
-</div>
+### Responsive
-<!-- Split dropend button -->
-<div class="btn-group dropend">
- <button type="button" class="btn btn-secondary">
- Split dropend
- </button>
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropend</span>
- </button>
- <ul class="dropdown-menu">
- <!-- Dropdown menu links -->
- </ul>
-</div>
-```
+Change placement at different breakpoints using responsive prefixes. The syntax is `breakpoint:placement`, where breakpoint is one of `sm`, `md`, `lg`, `xl`, or `2xl`. Multiple breakpoints can be combined in a single attribute, space-separated.
-### Dropstart
+For example, `data-bs-placement="bottom-start md:bottom-end lg:right-start"` will:
-Trigger dropdown menus at the left of the elements by adding `.dropstart` to the parent element.
+- Show the menu at `bottom-start` on small screens (default)
+- Switch to `bottom-end` at the `md` breakpoint
+- Switch to `right-start` at the `lg` breakpoint
-<Example showMarkup={false} code={`<div class="btn-group dropstart">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropstart
+<Example class="d-flex gap-2" code={`<div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="bottom-start md:bottom-end" aria-expanded="false">
+ Bottom start → md:bottom-end
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
</div>
- <div class="btn-group dropstart">
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropstart</span>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="bottom lg:right" aria-expanded="false">
+ Bottom → lg:right
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Action</a></li>
+ <li><a class="dropdown-item" href="#">Another action</a></li>
+ </ul>
+ </div>
+ <div class="dropdown">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-placement="top-end md:right-start xl:bottom-start" aria-expanded="false">
+ Multi-breakpoint
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- <li><hr class="dropdown-divider"></li>
- <li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
- <button type="button" class="btn btn-secondary">Split dropstart</button>
</div>`} />
-```html
-<!-- Default dropstart button -->
-<div class="btn-group dropstart">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropstart
- </button>
- <ul class="dropdown-menu">
- <!-- Dropdown menu links -->
- </ul>
-</div>
-
-<!-- Split dropstart button -->
-<div class="btn-group dropstart">
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="visually-hidden">Toggle Dropstart</span>
- </button>
- <ul class="dropdown-menu">
- <!-- Dropdown menu links -->
- </ul>
- <button type="button" class="btn btn-secondary">
- Split dropstart
- </button>
-</div>
-```
+Resize your browser window to see the placement change at different breakpoints.
## Menu items
You can use `<a>` or `<button>` elements as dropdown items.
+<Callout>
+We use utility classes to display dropdown menus in our docs examples, but they're not required for your own use.
+</Callout>
+
<Example code={`<div class="dropdown">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown
</button>
<ul class="dropdown-menu">
You can also create non-interactive dropdown items with `.dropdown-item-text`. Feel free to style further with custom CSS or text utilities.
-<Example code={`<ul class="dropdown-menu">
+<Example code={`<ul class="dropdown-menu show position-static">
<li><span class="dropdown-item-text">Dropdown item text</span></li>
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
### Active
-Add `.active` to items in the dropdown to **style them as active**. To convey the active state to assistive technologies, use the `aria-current` attribute — using the `page` value for the current page, or `true` for the current item in a set.
+Add `.active` to items in the dropdown to style them as the active selection. To convey the active state to assistive technologies, use the `aria-current` attribute — using the `page` value for the current page, or `true` for the current item in a set.
-<Example code={`<ul class="dropdown-menu">
+<Example code={`<ul class="dropdown-menu show position-static">
<li><a class="dropdown-item" href="#">Regular link</a></li>
<li><a class="dropdown-item active" href="#" aria-current="true">Active link</a></li>
<li><a class="dropdown-item" href="#">Another link</a></li>
### Disabled
-Add `.disabled` to items in the dropdown to **style them as disabled**.
+Add `.disabled` to items in the dropdown to style them as disabled.
-<Example code={`<ul class="dropdown-menu">
+<Example code={`<ul class="dropdown-menu show position-static">
<li><a class="dropdown-item" href="#">Regular link</a></li>
<li><a class="dropdown-item disabled" aria-disabled="true">Disabled link</a></li>
<li><a class="dropdown-item" href="#">Another link</a></li>
</ul>`} />
-## Menu alignment
-
-By default, a dropdown menu is automatically positioned 100% from the top and along the left side of its parent. You can change this with the directional `.drop*` classes, but you can also control them with additional modifier classes.
-
-Add `.dropdown-menu-end` to a `.dropdown-menu` to right align the dropdown menu. Directions are mirrored when using Bootstrap in RTL, meaning `.dropdown-menu-end` will appear on the left side.
-
-<Callout>
-**Heads up!** Dropdowns are positioned thanks to Popper except when they are contained in a navbar.
-</Callout>
-
-<Example code={`<div class="btn-group">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Right-aligned menu example
- </button>
- <ul class="dropdown-menu dropdown-menu-end">
- <li><button class="dropdown-item" type="button">Action</button></li>
- <li><button class="dropdown-item" type="button">Another action</button></li>
- <li><button class="dropdown-item" type="button">Something else here</button></li>
- </ul>
- </div>`} />
-
-### Responsive alignment
-
-If you want to use responsive alignment, disable dynamic positioning by adding the `data-bs-display="static"` attribute and use the responsive variation classes.
-
-To align **right** the dropdown menu with the given breakpoint or larger, add `.dropdown-menu{-sm|-md|-lg|-xl|-2xl}-end`.
-
-<Example code={`<div class="btn-group">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
- Left-aligned but right aligned when large screen
- </button>
- <ul class="dropdown-menu dropdown-menu-lg-end">
- <li><button class="dropdown-item" type="button">Action</button></li>
- <li><button class="dropdown-item" type="button">Another action</button></li>
- <li><button class="dropdown-item" type="button">Something else here</button></li>
- </ul>
- </div>`} />
-
-To align **left** the dropdown menu with the given breakpoint or larger, add `.dropdown-menu-end` and `.dropdown-menu{-sm|-md|-lg|-xl|-2xl}-start`.
-
-<Example code={`<div class="btn-group">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
- Right-aligned but left aligned when large screen
- </button>
- <ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start">
- <li><button class="dropdown-item" type="button">Action</button></li>
- <li><button class="dropdown-item" type="button">Another action</button></li>
- <li><button class="dropdown-item" type="button">Something else here</button></li>
- </ul>
- </div>`} />
-
-Note that you don’t need to add a `data-bs-display="static"` attribute to dropdown buttons in navbars, since Popper isn’t used in navbars.
-
-### Alignment options
-
-Taking most of the options shown above, here’s a small kitchen sink demo of various dropdown alignment options in one place.
-
-<Example code={`<div class="btn-group">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
- Dropdown
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- </ul>
- </div>
-
- <div class="btn-group">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Right-aligned menu
- </button>
- <ul class="dropdown-menu dropdown-menu-end">
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- </ul>
- </div>
-
- <div class="btn-group">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
- Left-aligned, right-aligned lg
- </button>
- <ul class="dropdown-menu dropdown-menu-lg-end">
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- </ul>
- </div>
-
- <div class="btn-group">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
- Right-aligned, left-aligned lg
- </button>
- <ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start">
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- </ul>
- </div>
-
- <div class="btn-group dropstart">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropstart
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- </ul>
- </div>
-
- <div class="btn-group dropend">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropend
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- </ul>
- </div>
-
- <div class="btn-group dropup">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
- Dropup
- </button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- <li><a class="dropdown-item" href="#">Menu item</a></li>
- </ul>
- </div>`} />
-
## Menu content
### Headers
<Example code={`<div class="d-flex">
<div class="dropdown me-1">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" data-bs-offset="10,20">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-toggle="dropdown" aria-expanded="false" data-bs-offset="10,20">
Offset
</button>
<ul class="dropdown-menu">
</ul>
</div>
<div class="btn-group">
- <button type="button" class="btn btn-secondary">Reference</button>
- <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" data-bs-reference="parent">
+ <button type="button" class="btn btn-solid theme-secondary">Reference</button>
+ <button type="button" class="btn btn-solid theme-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" data-bs-reference="parent">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
By default, the dropdown menu is closed when clicking inside or outside the dropdown menu. You can use the `autoClose` option to change this behavior of the dropdown.
<Example code={`<div class="btn-group">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="true" aria-expanded="false">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-auto-close="true" aria-expanded="false">
Default dropdown
</button>
<ul class="dropdown-menu">
</div>
<div class="btn-group">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="inside" aria-expanded="false">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-auto-close="inside" aria-expanded="false">
Clickable inside
</button>
<ul class="dropdown-menu">
</div>
<div class="btn-group">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
Clickable outside
</button>
<ul class="dropdown-menu">
</div>
<div class="btn-group">
- <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="false" aria-expanded="false">
+ <button class="btn btn-solid theme-secondary" type="button" data-bs-toggle="dropdown" data-bs-auto-close="false" aria-expanded="false">
Manual close
</button>
<ul class="dropdown-menu">
Call the dropdowns via JavaScript:
```js
-const dropdownElementList = document.querySelectorAll('.dropdown-toggle')
+const dropdownElementList = document.querySelectorAll('[data-bs-toggle="dropdown"]')
const dropdownList = [...dropdownElementList].map(dropdownToggleEl => new bootstrap.Dropdown(dropdownToggleEl))
```
+### Dependencies
+
+The dropdown plugin requires the following JavaScript files if you're building Bootstrap's JS from source:
+
+<BsTable>
+| File | Description |
+| --- | --- |
+| `js/src/dropdown.js` | Main dropdown component |
+| `js/src/base-component.js` | Base component class |
+| `js/src/dom/event-handler.js` | Event handling utilities |
+| `js/src/dom/manipulator.js` | Data attribute manipulation |
+| `js/src/dom/selector-engine.js` | DOM selector utilities |
+| `js/src/util/index.js` | Core utility functions |
+| `js/src/util/floating-ui.js` | Responsive placement utilities |
+| `@floating-ui/dom` | Third-party positioning library |
+</BsTable>
+
### Options
<JsDataAttributes />
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `autoClose` | boolean, string | `true` | Configure the auto close behavior of the dropdown: <ul class="my-2"><li>`true` - the dropdown will be closed by clicking outside or inside the dropdown menu.</li><li>`false` - the dropdown will be closed by clicking the toggle button and manually calling `hide` or `toggle` method. (Also will not be closed by pressing <kbd>Esc</kbd> key)</li><li>`'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu.</li> <li>`'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu.</li></ul> Note: the dropdown can always be closed with the <kbd>Esc</kbd> key. |
-| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the dropdown menu (applies only to Popper’s preventOverflow modifier). By default it’s `clippingParents` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Popper’s [detectOverflow docs](https://popper.js.org/docs/v2/utils/detect-overflow/#boundary). |
-| `display` | string | `'dynamic'` | By default, we use Popper for dynamic positioning. Disable this with `static`. |
-| `offset` | array, string, function | `[0, 2]` | Offset of the dropdown relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the popper placement, the reference, and popper rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [skidding](https://popper.js.org/docs/v2/modifiers/offset/#skidding-1), [distance](https://popper.js.org/docs/v2/modifiers/offset/#distance-1). For more information refer to Popper’s [offset docs](https://popper.js.org/docs/v2/modifiers/offset/#options). |
-| `popperConfig` | null, object, function | `null` | To change Bootstrap’s default Popper config, see [Popper’s configuration](https://popper.js.org/docs/v2/constructors/#options). When a function is used to create the Popper configuration, it’s called with an object that contains the Bootstrap’s default Popper configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Popper. |
-| `reference` | string, element, object | `'toggle'` | Reference element of the dropdown menu. Accepts the values of `'toggle'`, `'parent'`, an HTMLElement reference or an object providing `getBoundingClientRect`. For more information refer to Popper’s [constructor docs](https://popper.js.org/docs/v2/constructors/#createpopper) and [virtual element docs](https://popper.js.org/docs/v2/virtual-elements/). |
+| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the dropdown menu (applies only to the shift middleware). By default it's `clippingParents` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Floating UI's [shift docs](https://floating-ui.com/docs/shift). |
+| `display` | string | `'dynamic'` | By default, we use Floating UI for dynamic positioning. Disable this with `static`. |
+| `offset` | array, string, function | `[0, 2]` | Offset of the dropdown relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the placement, the reference, and floating rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [skidding, distance]. For more information refer to Floating UI's [offset docs](https://floating-ui.com/docs/offset). |
+| `floatingConfig` | null, object, function | `null` | To change Bootstrap's default Floating UI config, see [Floating UI's configuration](https://floating-ui.com/docs/computePosition). When a function is used to create the Floating UI configuration, it's called with an object that contains the Bootstrap's default Floating UI configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Floating UI. |
+| `placement` | string | `'bottom-start'` | Placement of the dropdown menu. Can be any valid Floating UI placement: `'top'`, `'top-start'`, `'top-end'`, `'bottom'`, `'bottom-start'`, `'bottom-end'`, `'right'`, `'right-start'`, `'right-end'`, `'left'`, `'left-start'`, `'left-end'`. Supports responsive prefixes like `'bottom-start md:bottom-end lg:right'` to change placement at different breakpoints. |
+| `reference` | string, element, object | `'toggle'` | Reference element of the dropdown menu. Accepts the values of `'toggle'`, `'parent'`, an HTMLElement reference or an object providing `getBoundingClientRect`. For more information refer to Floating UI's [virtual elements docs](https://floating-ui.com/docs/virtual-elements). |
</BsTable>
-#### Using function with `popperConfig`
+#### Using function with `floatingConfig`
```js
const dropdown = new bootstrap.Dropdown(element, {
- popperConfig(defaultBsPopperConfig) {
- // const newPopperConfig = {...}
- // use defaultBsPopperConfig if needed...
- // return newPopperConfig
+ floatingConfig(defaultBsFloatingConfig) {
+ // const newFloatingConfig = {...}
+ // use defaultBsFloatingConfig if needed...
+ // return newFloatingConfig
}
})
```
---
-title: Popovers
-description: Documentation and examples for adding Bootstrap popovers, like those found in iOS, to any element on your site.
+title: Popover
+description: Small, contextual overlays for displaying information on click via JavaScript. Extends our [Tooltip]([[docsref:/components/tooltip]]) plugin, which is powered by [Floating UI](https://floating-ui.com/).
toc: true
+aliases:
+ - "/docs/[[config:docs_version]]/components/popovers/"
---
## Overview
+Click the button below to trigger a popover:
+
+<Example addStackblitzJs code={`<button type="button" class="btn btn-solid theme-primary"
+ data-bs-toggle="popover"
+ data-bs-title="Popover title"
+ data-bs-content="This is the popover content—make it as long as you want.">
+ Toggle popover
+ </button>`} />
+
Things to know when using the popover plugin:
-- Popovers rely on the third party library [Popper](https://popper.js.org/docs/v2/) for positioning. You must include [popper.min.js]([[config:cdn.popper]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Popper.
-- Popovers require the [popover plugin]([[docsref:/components/popovers]]) as a dependency.
-- Popovers are opt-in for performance reasons, so **you must initialize them yourself**.
+- Popovers rely on the third party library [Floating UI](https://floating-ui.com/) for positioning. You must include [floating-ui.dom.umd.min.js]([[config:cdn.floating_ui]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Floating UI.
+- Popovers require the [popover plugin]([[docsref:/components/popover]]) as a dependency.
+- Popovers are automatically initialized when using `data-bs-toggle="popover"`.
- Zero-length `title` and `content` values will never show a popover.
- Specify `container: 'body'` to avoid rendering problems in more complex components (like our input groups, button groups, etc).
- Triggering popovers on hidden elements will not work.
## Examples
-### Enable popovers
-
-As mentioned above, you must initialize popovers before they can be used. One way to initialize all popovers on a page would be to select them by their `data-bs-toggle` attribute, like so:
-
-```js
-const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
-const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
-```
-
### Live demo
-We use JavaScript similar to the snippet above to render the following live popover. Titles are set via `data-bs-title` and body content is set via `data-bs-content`.
+Titles are set via `data-bs-title` and body content is set via `data-bs-content`.
<Callout name="warning-data-bs-title-vs-title" type="warning" />
-<Example addStackblitzJs code={`<button type="button" class="btn btn-lg btn-danger" data-bs-toggle="popover" data-bs-title="Popover title" data-bs-content="And here’s some amazing content. It’s very engaging. Right?">Click to toggle popover</button>`} />
+<Example addStackblitzJs code={`<button type="button" class="btn btn-solid theme-primary"
+ data-bs-toggle="popover"
+ data-bs-title="Popover title"
+ data-bs-content="And here’s some amazing content. It’s very engaging. Right?">
+ Toggle popover
+ </button>`} />
-### Four directions
+### Placement
-Four options are available: top, right, bottom, and left. Directions are mirrored when using Bootstrap in RTL. Set `data-bs-placement` to change the direction.
+Set `data-bs-placement` to change the direction. Four options are available: top, right, bottom, and left. Directions are mirrored when using Bootstrap in RTL.
-<Example addStackblitzJs code={`<button type="button" class="btn btn-secondary" data-bs-container="body" data-bs-toggle="popover" data-bs-placement="top" data-bs-content="Top popover">
- Popover on top
+<Example addStackblitzJs code={`<button type="button" class="btn btn-solid theme-secondary"
+ data-bs-container="body"
+ data-bs-toggle="popover"
+ data-bs-placement="left"
+ data-bs-content="Left popover">
+ Popover on left
</button>
- <button type="button" class="btn btn-secondary" data-bs-container="body" data-bs-toggle="popover" data-bs-placement="right" data-bs-content="Right popover">
- Popover on right
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-container="body"
+ data-bs-toggle="popover"
+ data-bs-placement="top"
+ data-bs-content="Top popover">
+ Popover on top
</button>
- <button type="button" class="btn btn-secondary" data-bs-container="body" data-bs-toggle="popover" data-bs-placement="bottom" data-bs-content="Bottom popover">
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-container="body"
+ data-bs-toggle="popover"
+ data-bs-placement="bottom"
+ data-bs-content="Bottom popover">
Popover on bottom
</button>
- <button type="button" class="btn btn-secondary" data-bs-container="body" data-bs-toggle="popover" data-bs-placement="left" data-bs-content="Left popover">
- Popover on left
+
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-container="body"
+ data-bs-toggle="popover"
+ data-bs-placement="right"
+ data-bs-content="Right popover">
+ Popover on right
+ </button>`} />
+
+### Responsive placement
+
+Change placement at different breakpoints using responsive prefixes. The syntax is `breakpoint:placement`, where breakpoint is one of `sm`, `md`, `lg`, `xl`, or `2xl`.
+
+<Example addStackblitzJs code={`<button type="button" class="btn btn-solid theme-secondary"
+ data-bs-container="body"
+ data-bs-toggle="popover"
+ data-bs-placement="top md:right lg:bottom"
+ data-bs-content="This popover changes placement at different breakpoints.">
+ Top → md:right → lg:bottom
+ </button>
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-container="body"
+ data-bs-toggle="popover"
+ data-bs-placement="left lg:top"
+ data-bs-content="Responsive popover placement.">
+ Left → lg:top
</button>`} />
+Resize your browser to see the popover placement change at different breakpoints.
+
### Custom `container`
When you have some styles on a parent element that interfere with a popover, you’ll want to specify a custom `container` so that the popover’s HTML appears within that element instead. This is common in responsive tables, input groups, and the like.
<ScssDocs name="custom-popovers" file="site/src/scss/_component-examples.scss" />
-<Example addStackblitzJs class="custom-popover-demo" code={`<button type="button" class="btn btn-secondary"
+<Example addStackblitzJs class="custom-popover-demo" code={`<button type="button" class="btn btn-solid theme-secondary"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-custom-class="custom-popover"
data-bs-title="Custom popover"
**Dismissing on next click requires specific HTML for proper cross-browser and cross-platform behavior.** You can only use `<a>` elements, not `<button>`s, and you must include a [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex).
</Callout>
-<Example addStackblitzJs code={`<a tabindex="0" class="btn btn-lg btn-danger" role="button" data-bs-toggle="popover" data-bs-trigger="focus" data-bs-title="Dismissible popover" data-bs-content="And here’s some amazing content. It’s very engaging. Right?">Dismissible popover</a>`} />
+<Example addStackblitzJs code={`<a tabindex="0" class="btn btn-solid theme-secondary" role="button"
+ data-bs-toggle="popover"
+ data-bs-trigger="focus"
+ data-bs-title="Dismissible popover"
+ data-bs-content="And here’s some amazing content. It’s very engaging. Right?">
+ Dismissible popover
+ </a>`} />
```js
const popover = new bootstrap.Popover('.popover-dismiss', {
For disabled popover triggers, you may also prefer `data-bs-trigger="hover focus"` so that the popover appears as immediate visual feedback to your users as they may not expect to _click_ on a disabled element.
-<Example addStackblitzJs code={`<span class="d-inline-block" tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-content="Disabled popover">
+<Example addStackblitzJs code={`<span class="d-inline-block rounded-3" tabindex="0"
+ data-bs-toggle="popover"
+ data-bs-trigger="hover focus"
+ data-bs-content="Disabled popover">
<button class="btn btn-primary" type="button" disabled>Disabled button</button>
</span>`} />
Popovers do not manage keyboard focus order, and their placement can be random in the DOM, so be careful when adding interactive elements (like forms or links), as it may lead to an illogical focus order or make the popover content itself completely unreachable for keyboard users. In cases where you must use these elements, consider using a dialog instead.
</Callout>
+### Dependencies
+
+The popover plugin extends the tooltip plugin, so it requires all tooltip dependencies plus its own:
+
+<BsTable>
+| File | Description |
+| --- | --- |
+| `js/src/popover.js` | Main popover component |
+| `js/src/tooltip.js` | Tooltip component (extended by popover) |
+| `js/src/base-component.js` | Base component class |
+| `js/src/dom/event-handler.js` | Event handling utilities |
+| `js/src/dom/manipulator.js` | Data attribute manipulation |
+| `js/src/util/index.js` | Core utility functions |
+| `js/src/util/sanitizer.js` | HTML content sanitizer |
+| `js/src/util/template-factory.js` | Template rendering utilities |
+| `js/src/util/floating-ui.js` | Responsive placement utilities |
+| `@floating-ui/dom` | Third-party positioning library |
+</BsTable>
+
### Options
<JsDataAttributes />
| --- | --- | --- | --- |
| `allowList` | object | [Default value]([[docsref:/getting-started/javascript#sanitizer]]) | An object containing allowed tags and attributes. Those not explicitly allowed will be removed by [the content sanitizer]([[docsref:/getting-started/javascript#sanitizer]]). <Callout type="warning">**Exercise caution when adding to this list.** Refer to [OWASP’s Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) for more information.</Callout> |
| `animation` | boolean | `true` | Apply a CSS fade transition to the popover. |
-| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the popover (applies only to Popper’s preventOverflow modifier). By default, it’s `'clippingParents'` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Popper’s [detectOverflow docs](https://popper.js.org/docs/v2/utils/detect-overflow/#boundary). |
+| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the popover (applies only to Floating UI's shift middleware). By default, it’s `'clippingParents'` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Floating UI's [shift docs](https://floating-ui.com/docs/shift#boundary). |
| `container` | string, element, false | `false` | Appends the popover to a specific element. Example: `container: 'body'`. This option is particularly useful in that it allows you to position the popover in the flow of the document near the triggering element - which will prevent the popover from floating away from the triggering element during a window resize. |
| `content` | string, element, function | `''` | The popover’s text content. If a function is given, it will be called with its `this` reference set to the element that the popover is attached to. |
| `customClass` | string, function | `''` | Add classes to the popover when it is shown. Note that these classes will be added in addition to any classes specified in the template. To add multiple classes, separate them with spaces: `'class-1 class-2'`. You can also pass a function that should return a single string containing additional class names. |
| `delay` | number, object | `0` | Delay showing and hiding the popover (ms)—doesn’t apply to manual trigger type. If a number is supplied, delay is applied to both hide/show. Object structure is: `delay: { "show": 500, "hide": 100 }`. |
-| `fallbackPlacements` | string, array | `['top', 'right', 'bottom', 'left']` | Define fallback placements by providing a list of placements in array (in order of preference). For more information refer to Popper’s [behavior docs](https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements). |
+| `fallbackPlacements` | array | `['top', 'right', 'bottom', 'left']` | Define fallback placements by providing a list of placements in array (in order of preference). For more information refer to Floating UI's [flip docs](https://floating-ui.com/docs/flip#fallbackplacements). |
| `html` | boolean | `false` | Allow HTML in the popover. If true, HTML tags in the popover’s `title` will be rendered in the popover. If false, `innerText` property will be used to insert content into the DOM. Prefer text when dealing with user-generated input to [prevent XSS attacks](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html). |
-| `offset` | number, string, function | `[0, 8]` | Offset of the popover relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the popper placement, the reference, and popper rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [skidding](https://popper.js.org/docs/v2/modifiers/offset/#skidding-1), [distance](https://popper.js.org/docs/v2/modifiers/offset/#distance-1). For more information refer to Popper’s [offset docs](https://popper.js.org/docs/v2/modifiers/offset/#options). |
-| `placement` | string, function | `'right'` | How to position the popover: auto, top, bottom, left, right. When `auto` is specified, it will dynamically reorient the popover. When a function is used to determine the placement, it is called with the popover DOM node as its first argument and the triggering element DOM node as its second. The `this` context is set to the popover instance. |
-| `popperConfig` | null, object, function | `null` | To change Bootstrap’s default Popper config, see [Popper’s configuration](https://popper.js.org/docs/v2/constructors/#options). When a function is used to create the Popper configuration, it’s called with an object that contains the Bootstrap’s default Popper configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Popper. |
+| `offset` | array, string, function | `[0, 8]` | Offset of the popover relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the floating placement, the reference, and floating rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [crossAxis](https://floating-ui.com/docs/offset#crossaxis), [mainAxis](https://floating-ui.com/docs/offset#mainaxis). For more information refer to Floating UI's [offset docs](https://floating-ui.com/docs/offset). |
+| `placement` | string, function | `'right'` | How to position the popover: top, bottom, left, right. Supports responsive prefixes like `'top md:right lg:bottom'` to change placement at different breakpoints. When a function is used to determine the placement, it is called with the popover DOM node as its first argument and the triggering element DOM node as its second. |
+| `floatingConfig` | null, object, function | `null` | To change Bootstrap's default Floating UI config, see [Floating UI's configuration](https://floating-ui.com/docs/computePosition). When a function is used to create the Floating UI configuration, it’s called with an object that contains the Bootstrap's default Floating UI configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Floating UI. |
| `sanitize` | boolean | `true` | Enable [content sanitization]([[docsref:/getting-started/javascript#sanitizer]]). If true, the `template`, `content` and `title` options will be sanitized. <Callout type="warning">**Exercise caution when disabling content sanitization.** Refer to [OWASP’s Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) for more information. Vulnerabilities caused solely by disabling content sanitization are not considered within scope for Bootstrap’s security model.</Callout> |
| `sanitizeFn` | null, function | `null` | Provide an alternative [content sanitization]([[docsref:/getting-started/javascript#sanitizer]]) function. This can be useful if you prefer to use a dedicated library to perform sanitization. |
| `selector` | string, false | `false` | If a selector is provided, popover objects will be delegated to the specified targets. In practice, this is used to also apply popovers to dynamically added DOM elements (`jQuery.on` support). See [this issue]([[config:repo]]/issues/4215) and [an informative example](https://codepen.io/Johann-S/pen/djJYPb). **Note**: `title` attribute must not be used as a selector. |
Options for individual popovers can alternatively be specified through the use of data attributes, as explained above.
</Callout>
-#### Using function with `popperConfig`
+#### Using function with `floatingConfig`
```js
const popover = new bootstrap.Popover(element, {
- popperConfig(defaultBsPopperConfig) {
- // const newPopperConfig = {...}
- // use defaultBsPopperConfig if needed...
- // return newPopperConfig
+ floatingConfig(defaultBsFloatingConfig) {
+ // const newFloatingConfig = {...}
+ // use defaultBsFloatingConfig if needed...
+ // return newFloatingConfig
}
})
```
---
title: Tooltips
-description: Documentation and examples for adding custom Bootstrap tooltips with CSS and JavaScript using CSS3 for animations and data-bs-attributes for local title storage.
+description: Custom replacement for browser tooltips—themed with CSS variables and powered by [Floating UI](https://floating-ui.com/).
+aliases:
+ - "/docs/[[config:docs_version]]/components/tooltips/"
toc: true
---
## Overview
+Hover the button below to see a tooltip. By default, the tooltip will appear above their trigger element (anything with the `data-bs-toggle="tooltip"` attribute).
+
+<Example addStackblitzJs class="tooltip-demo" code={`<button type="button" class="btn btn-solid theme-primary"
+ data-bs-toggle="tooltip"
+ data-bs-title="Tooltip on top">
+ Example tooltip
+ </button>`} />
+
Things to know when using the tooltip plugin:
-- Tooltips rely on the third party library [Popper](https://popper.js.org/docs/v2/) for positioning. You must include [popper.min.js]([[config:cdn.popper]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Popper.
-- Tooltips are opt-in for performance reasons, so **you must initialize them yourself**.
+- Tooltips rely on the third party library [Floating UI](https://floating-ui.com/) for positioning. You must include [floating-ui.dom.umd.min.js]([[config:cdn.floating_ui]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Floating UI.
+- Tooltips are automatically initialized when using `data-bs-toggle="tooltip"`.
- Tooltips with zero-length titles are never displayed.
- Specify `container: 'body'` to avoid rendering problems in more complex components (like our input groups, button groups, etc).
- Triggering tooltips on hidden elements will not work.
## Examples
-### Enable tooltips
-
-As mentioned above, you must initialize tooltips before they can be used. One way to initialize all tooltips on a page would be to select them by their `data-bs-toggle` attribute, like so:
-
-```js
-const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
-const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
-```
-
### Tooltips on links
Hover over the links below to see tooltips:
-<Example addStackblitzJs class="tooltip-demo" code={`<p class="muted">Placeholder text to demonstrate some <a href="#" data-bs-toggle="tooltip" data-bs-title="Default tooltip">inline links</a> with tooltips. This is now just filler, no killer. Content placed here just to mimic the presence of <a href="#" data-bs-toggle="tooltip" data-bs-title="Another tooltip">real text</a>. And all that just to give you an idea of how tooltips would look when used in real-world situations. So hopefully you’ve now seen how <a href="#" data-bs-toggle="tooltip" data-bs-title="Another one here too">these tooltips on links</a> can work in practice, once you use them on <a href="#" data-bs-toggle="tooltip" data-bs-title="The last tip!">your own</a> site or project.</p>`} />
+<Example addStackblitzJs class="tooltip-demo" code={`<p class="fg-3">Placeholder text to demonstrate some <a href="#" data-bs-toggle="tooltip" data-bs-title="Default tooltip">inline links</a> with tooltips. This is now just filler, no killer. Content placed here just to mimic the presence of <a href="#" data-bs-toggle="tooltip" data-bs-title="Another tooltip">real text</a>. And all that just to give you an idea of how tooltips would look when used in real-world situations. So hopefully you’ve now seen how <a href="#" data-bs-toggle="tooltip" data-bs-title="Another one here too">these tooltips on links</a> can work in practice, once you use them on <a href="#" data-bs-toggle="tooltip" data-bs-title="The last tip!">your own</a> site or project.</p>`} />
<Callout name="warning-data-bs-title-vs-title" type="warning" />
<ScssDocs name="custom-tooltip" file="site/src/scss/_component-examples.scss" />
-<Example addStackblitzJs class="tooltip-demo" code={`<button type="button" class="btn btn-secondary"
+<Example addStackblitzJs class="tooltip-demo" code={`<button type="button" class="btn btn-solid theme-secondary"
data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="This top tooltip is themed via CSS variables.">
Custom tooltip
</button>`} />
-### Directions
+### Placement
+
+Set `data-bs-placement` to change the direction. Hover over the buttons below to see the four tooltips directions: top, right, bottom, and left. Directions are mirrored when using Bootstrap in RTL.
+
+<Example class="tooltip-demo" code={`<button type="button" class="btn btn-solid theme-secondary"
+ data-bs-toggle="tooltip"
+ data-bs-placement="left"
+ data-bs-title="Tooltip on left">
+ Tooltip on left
+ </button>
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-toggle="tooltip"
+ data-bs-placement="top"
+ data-bs-title="Tooltip on top">
+ Tooltip on top
+ </button>
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-toggle="tooltip"
+ data-bs-placement="bottom"
+ data-bs-title="Tooltip on bottom">
+ Tooltip on bottom
+ </button>
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-toggle="tooltip"
+ data-bs-placement="right"
+ data-bs-title="Tooltip on right">
+ Tooltip on right
+ </button>`} />
-Hover over the buttons below to see the four tooltips directions: top, right, bottom, and left. Directions are mirrored when using Bootstrap in RTL.
+### Responsive placement
+
+Change placement at different breakpoints using responsive prefixes. The syntax is `breakpoint:placement`, where breakpoint is one of `sm`, `md`, `lg`, `xl`, or `2xl`.
+
+<Example class="tooltip-demo" code={`<button type="button" class="btn btn-solid theme-secondary"
+ data-bs-toggle="tooltip"
+ data-bs-placement="top md:right lg:bottom"
+ data-bs-title="Responsive tooltip">
+ Top → md:right → lg:bottom
+ </button>
+ <button type="button" class="btn btn-solid theme-secondary"
+ data-bs-toggle="tooltip"
+ data-bs-placement="left lg:top"
+ data-bs-title="Responsive tooltip">
+ Left → lg:top
+ </button>`} />
-<Example showMarkup={false} class="tooltip-demo" code={`<div class="bd-example-tooltips">
- <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Tooltip on top">Tooltip on top</button>
- <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Tooltip on right">Tooltip on right</button>
- <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Tooltip on bottom">Tooltip on bottom</button>
- <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Tooltip on left">Tooltip on left</button>
- <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-html="true" data-bs-title="<em>Tooltip</em> <u>with</u> <b>HTML</b>">Tooltip with HTML</button>
- </div>`} />
+Resize your browser to see the tooltip placement change at different breakpoints.
-```html
-<button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Tooltip on top">
- Tooltip on top
-</button>
-<button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Tooltip on right">
- Tooltip on right
-</button>
-<button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Tooltip on bottom">
- Tooltip on bottom
-</button>
-<button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Tooltip on left">
- Tooltip on left
-</button>
-```
+### Custom HTML
And with custom HTML added:
-```html
-<button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-html="true" data-bs-title="<em>Tooltip</em> <u>with</u> <b>HTML</b>">
- Tooltip with HTML
-</button>
-```
+<Example class="tooltip-demo" code={`<button type="button" class="btn btn-solid theme-secondary"
+ data-bs-toggle="tooltip"
+ data-bs-html="true"
+ data-bs-title="<em>Tooltip</em> <u>with</u> <b>HTML</b>">
+ Tooltip with HTML
+ </button>`} />
With an SVG:
-<div class="bd-example tooltip-demo">
- <a href="#" class="d-inline-block" data-bs-toggle="tooltip" data-bs-title="Default tooltip" aria-label="Hover or focus to see default tooltip">
+<Example class="tooltip-demo" code={`<a href="#" class="d-inline-block"
+ data-bs-toggle="tooltip"
+ data-bs-title="Default tooltip"
+ aria-label="Hover or focus to see default tooltip">
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 100 100" aria-hidden="true">
<rect width="100%" height="100%" fill="#563d7c"/>
<circle cx="50" cy="50" r="30" fill="#007bff"/>
</svg>
- </a>
-</div>
+ </a>`} />
+
+### Disabled elements
+
+Elements with the `disabled` attribute aren’t interactive, meaning users cannot focus, hover, or click them to trigger a tooltip (or popover). As a workaround, you’ll want to trigger the tooltip from a wrapper `<div>` or `<span>`, ideally made keyboard-focusable using `tabindex="0"`.
+
+<Example class="tooltip-demo" addStackblitzJs code={`<span class="d-inline-block" tabindex="0"
+ data-bs-toggle="tooltip"
+ data-bs-title="Disabled tooltip">
+ <button class="btn btn-primary" type="button" disabled>Disabled button</button>
+ </span>`} />
## CSS
```
<Callout type="warning">
-Tooltips automatically attempt to change positions when a parent container has `overflow: auto` or `overflow: scroll`, but still keeps the original placement’s positioning. Set the [`boundary` option](https://popper.js.org/docs/v2/modifiers/flip/#boundary) (for the flip modifier using the `popperConfig` option) to any HTMLElement to override the default value, `'clippingParents'`, such as `document.body`:
+Tooltips automatically attempt to change positions when a parent container has `overflow: auto` or `overflow: scroll`, but still keeps the original placement’s positioning. Set the [`boundary` option](https://floating-ui.com/docs/shift#boundary) (for the shift middleware using the `floatingConfig` option) to any HTMLElement to override the default value, `'clippingParents'`, such as `document.body`:
```js
const tooltip = new bootstrap.Tooltip('#example', {
</div>
```
-### Disabled elements
+### Dependencies
-Elements with the `disabled` attribute aren’t interactive, meaning users cannot focus, hover, or click them to trigger a tooltip (or popover). As a workaround, you’ll want to trigger the tooltip from a wrapper `<div>` or `<span>`, ideally made keyboard-focusable using `tabindex="0"`.
+The tooltip plugin requires the following JavaScript files if you're building Bootstrap's JS from source:
-<Example class="tooltip-demo" addStackblitzJs code={`<span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip" data-bs-title="Disabled tooltip">
- <button class="btn btn-primary" type="button" disabled>Disabled button</button>
- </span>`} />
+<BsTable>
+| File | Description |
+| --- | --- |
+| `js/src/tooltip.js` | Main tooltip component |
+| `js/src/base-component.js` | Base component class |
+| `js/src/dom/event-handler.js` | Event handling utilities |
+| `js/src/dom/manipulator.js` | Data attribute manipulation |
+| `js/src/util/index.js` | Core utility functions |
+| `js/src/util/sanitizer.js` | HTML content sanitizer |
+| `js/src/util/template-factory.js` | Template rendering utilities |
+| `js/src/util/floating-ui.js` | Responsive placement utilities |
+| `@floating-ui/dom` | Third-party positioning library |
+</BsTable>
### Options
| --- | --- | --- | --- |
| `allowList` | object | [Default value]([[docsref:/getting-started/javascript#sanitizer]]) | An object containing allowed tags and attributes. Those not explicitly allowed will be removed by [the content sanitizer]([[docsref:/getting-started/javascript#sanitizer]]). <Callout type="warning">**Exercise caution when adding to this list.** Refer to [OWASP’s Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) for more information.</Callout> |
| `animation` | boolean | `true` | Apply a CSS fade transition to the tooltip. |
-| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the tooltip (applies only to Popper’s preventOverflow modifier). By default, it’s `'clippingParents'` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Popper’s [detectOverflow docs](https://popper.js.org/docs/v2/utils/detect-overflow/#boundary). |
+| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the tooltip (applies only to Floating UI's shift middleware). By default, it’s `'clippingParents'` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Floating UI's [shift docs](https://floating-ui.com/docs/shift#boundary). |
| `container` | string, element, false | `false` | Appends the tooltip to a specific element. Example: `container: 'body'`. This option is particularly useful in that it allows you to position the tooltip in the flow of the document near the triggering element - which will prevent the tooltip from floating away from the triggering element during a window resize. |
| `customClass` | string, function | `''` | Add classes to the tooltip when it is shown. Note that these classes will be added in addition to any classes specified in the template. To add multiple classes, separate them with spaces: `'class-1 class-2'`. You can also pass a function that should return a single string containing additional class names. |
| `delay` | number, object | `0` | Delay showing and hiding the tooltip (ms)—doesn’t apply to manual trigger type. If a number is supplied, delay is applied to both hide/show. Object structure is: `delay: { "show": 500, "hide": 100 }`. |
-| `fallbackPlacements` | array | `['top', 'right', 'bottom', 'left']` | Define fallback placements by providing a list of placements in array (in order of preference). For more information refer to Popper’s [behavior docs](https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements). |
+| `fallbackPlacements` | array | `['top', 'right', 'bottom', 'left']` | Define fallback placements by providing a list of placements in array (in order of preference). For more information refer to Floating UI's [flip docs](https://floating-ui.com/docs/flip#fallbackplacements). |
| `html` | boolean | `false` | Allow HTML in the tooltip. If true, HTML tags in the tooltip’s `title` will be rendered in the tooltip. If false, `innerText` property will be used to insert content into the DOM. Prefer text when dealing with user-generated input to [prevent XSS attacks](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html). |
-| `offset` | array, string, function | `[0, 6]` | Offset of the tooltip relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the popper placement, the reference, and popper rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [skidding](https://popper.js.org/docs/v2/modifiers/offset/#skidding-1), [distance](https://popper.js.org/docs/v2/modifiers/offset/#distance-1). For more information refer to Popper’s [offset docs](https://popper.js.org/docs/v2/modifiers/offset/#options). |
-| `placement` | string, function | `'top'` | How to position the tooltip: auto, top, bottom, left, right. When `auto` is specified, it will dynamically reorient the tooltip. When a function is used to determine the placement, it is called with the tooltip DOM node as its first argument and the triggering element DOM node as its second. The `this` context is set to the tooltip instance. |
-| `popperConfig` | null, object, function | `null` | To change Bootstrap’s default Popper config, see [Popper’s configuration](https://popper.js.org/docs/v2/constructors/#options). When a function is used to create the Popper configuration, it’s called with an object that contains the Bootstrap’s default Popper configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Popper. |
+| `offset` | array, string, function | `[0, 6]` | Offset of the tooltip relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the floating placement, the reference, and floating rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [crossAxis](https://floating-ui.com/docs/offset#crossaxis), [mainAxis](https://floating-ui.com/docs/offset#mainaxis). For more information refer to Floating UI's [offset docs](https://floating-ui.com/docs/offset). |
+| `placement` | string, function | `'top'` | How to position the tooltip: top, bottom, left, right. Supports responsive prefixes like `'top md:right lg:bottom'` to change placement at different breakpoints. When a function is used to determine the placement, it is called with the tooltip DOM node as its first argument and the triggering element DOM node as its second. |
+| `floatingConfig` | null, object, function | `null` | To change Bootstrap’s default Floating UI config, see [Floating UI's configuration](https://floating-ui.com/docs/computePosition). When a function is used to create the Floating UI configuration, it’s called with an object that contains the Bootstrap’s default Floating UI configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Floating UI. |
| `sanitize` | boolean | `true` | Enable [content sanitization]([[docsref:/getting-started/javascript#sanitizer]]). If true, the `template`, `content` and `title` options will be sanitized. <Callout type="warning">**Exercise caution when disabling content sanitization.** Refer to [OWASP’s Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) for more information. Vulnerabilities caused solely by disabling content sanitization are not considered within scope for Bootstrap’s security model.</Callout> |
| `sanitizeFn` | null, function | `null` | Provide an alternative [content sanitization]([[docsref:/getting-started/javascript#sanitizer]]) function. This can be useful if you prefer to use a dedicated library to perform sanitization. |
| `selector` | string, false | `false` | If a selector is provided, tooltip objects will be delegated to the specified targets. In practice, this is used to also apply tooltips to dynamically added DOM elements (`jQuery.on` support). See [this issue]([[config:repo]]/issues/4215) and [an informative example](https://codepen.io/Johann-S/pen/djJYPb). **Note**: `title` attribute must not be used as a selector. |
Options for individual tooltips can alternatively be specified through the use of data attributes, as explained above.
</Callout>
-#### Using function with `popperConfig`
+#### Using function with `floatingConfig`
```js
const tooltip = new bootstrap.Tooltip(element, {
- popperConfig(defaultBsPopperConfig) {
- // const newPopperConfig = {...}
- // use defaultBsPopperConfig if needed...
- // return newPopperConfig
+ floatingConfig(defaultBsFloatingConfig) {
+ // const newFloatingConfig = {...}
+ // use defaultBsFloatingConfig if needed...
+ // return newFloatingConfig
}
})
```
## Responsive
-These Sass loops aren’t limited to color maps, either. You can also generate responsive variations of your components. Take for example our responsive alignment of the dropdowns where we mix an `@each` loop for the `$grid-breakpoints` Sass map with a media query include.
+These Sass loops aren't limited to color maps, either. You can also generate responsive variations of your components. Take for example our responsive navbar expand classes where we mix an `@each` loop for the `$grid-breakpoints` Sass map with a media query include.
-<ScssDocs name="responsive-breakpoints" file="scss/_dropdown.scss" />
+<ScssDocs name="navbar-expand-loop" file="scss/_navbar.scss" />
Should you modify your `$grid-breakpoints`, your changes will apply to all the loops iterating over that map.
<script src="[[config:cdn.js_bundle]]" integrity="[[config:cdn.js_bundle_hash]]" crossorigin="anonymous"></script>
```
-If you’re using our compiled JavaScript and prefer to include Popper separately, add Popper before our JS, via a CDN preferably.
+If you’re using our compiled JavaScript and prefer to include Floating UI separately, add Popper before our JS, via a CDN preferably.
```html
-<script src="[[config:cdn.popper]]" integrity="[[config:cdn.popper_hash]]" crossorigin="anonymous"></script>
+<script src="[[config:cdn.floating_ui]]" integrity="[[config:cdn.floating_ui_hash]]" crossorigin="anonymous"></script>
<script src="[[config:cdn.js]]" integrity="[[config:cdn.js_hash]]" crossorigin="anonymous"></script>
```
<script src="[[config:cdn.js_bundle]]" integrity="[[config:cdn.js_bundle_hash]]" crossorigin="anonymous"></script>
```
-If you’re using our compiled JavaScript and prefer to include Popper separately, add Popper before our JS, via a CDN preferably.
+If you’re using our compiled JavaScript and prefer to include Floating UI separately, add Popper before our JS, via a CDN preferably.
```html
-<script src="[[config:cdn.popper]]" integrity="[[config:cdn.popper_hash]]" crossorigin="anonymous"></script>
+<script src="[[config:cdn.floating_ui]]" integrity="[[config:cdn.floating_ui_hash]]" crossorigin="anonymous"></script>
<script src="[[config:cdn.js]]" integrity="[[config:cdn.js_hash]]" crossorigin="anonymous"></script>
```
- Buttons for toggling states and checkbox/radio functionality
- Carousel for all slide behaviors, controls, and indicators
- Collapse for toggling visibility of content
-- Dropdowns for displaying and positioning (also requires [Popper](https://popper.js.org/docs/v2/))
+- Dropdowns for displaying and positioning (also requires [Floating UI](https://floating-ui.com/))
- Modals for displaying, positioning, and scroll behavior
- Navbar for extending our Collapse and Offcanvas plugins to implement responsive behaviors
- Navs with the Tab plugin for toggling content panes
- Offcanvases for displaying, positioning, and scroll behavior
- Scrollspy for scroll behavior and navigation updates
- Toasts for displaying and dismissing
-- Tooltips and popovers for displaying and positioning (also requires [Popper](https://popper.js.org/docs/v2/))
+- Tooltips and popovers for displaying and positioning (also requires [Floating UI](https://floating-ui.com/))
## Individual or compiled
Compared to JS bundlers, using ESM in the browser requires you to use the full path and filename instead of the module name. [Read more about JS modules in the browser.](https://v8.dev/features/modules#specifiers) That’s why we use `'bootstrap.esm.min.js'` instead of `'bootstrap'` above. However, this is further complicated by our Popper dependency, which imports Popper into our JavaScript like so:
```js
-import * as Popper from "@popperjs/core"
+import * as Popper from "@floating-ui/dom"
```
If you try this as-is, you’ll see an error in the console like the following:
```text
-Uncaught TypeError: Failed to resolve module specifier "@popperjs/core". Relative references must start with either "/", "./", or "../".
+Uncaught TypeError: Failed to resolve module specifier "@floating-ui/dom". Relative references must start with either "/", "./", or "../".
```
To fix this, you can use an `importmap` to resolve the arbitrary module names to complete paths. If your [targeted browsers](https://caniuse.com/?search=importmap) do not support `importmap`, you’ll need to use the [es-module-shims](https://github.com/guybedford/es-module-shims) project. Here’s how it works for Bootstrap and Popper:
<script type="importmap">
{
"imports": {
- "@popperjs/core": "[[config:cdn.popper_esm]]",
+ "@floating-ui/dom": "[[config:cdn.floating_ui_esm]]",
"bootstrap": "https://cdn.jsdelivr.net/npm/bootstrap@[[config:current_version]]/dist/js/bootstrap.esm.min.js"
}
}
Some plugins and CSS components depend on other plugins. If you include plugins individually, make sure to check for these dependencies in the docs.
-Our dropdowns, popovers, and tooltips also depend on [Popper](https://popper.js.org/docs/v2/).
+Our dropdowns, popovers, and tooltips also depend on [Floating UI](https://floating-ui.com/).
## Data attributes
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
- <script src="[[config:cdn.popper]]" integrity="[[config:cdn.popper_hash]]" crossorigin="anonymous"></script>
+ <script src="[[config:cdn.floating_ui]]" integrity="[[config:cdn.floating_ui_hash]]" crossorigin="anonymous"></script>
<script src="[[config:cdn.js]]" integrity="[[config:cdn.js_hash]]" crossorigin="anonymous"></script>
-->
</body>
</html>
```
- You can also include [Popper](https://popper.js.org/docs/v2/) and our JS separately. If you don’t plan to use dropdowns, popovers, or tooltips, save some kilobytes by not including Popper.
+ You can also include [Floating UI](https://floating-ui.com/) and our JS separately. If you don’t plan to use dropdowns, popovers, or tooltips, save some kilobytes by not including Popper.
```html
- <script src="[[config:cdn.popper]]" integrity="[[config:cdn.popper_hash]]" crossorigin="anonymous"></script>
+ <script src="[[config:cdn.floating_ui]]" integrity="[[config:cdn.floating_ui_hash]]" crossorigin="anonymous"></script>
<script src="[[config:cdn.js]]" integrity="[[config:cdn.js_hash]]" crossorigin="anonymous"></script>
```
js_hash: z.string(),
js_bundle: z.string().url(),
js_bundle_hash: z.string(),
- popper: z.string().url(),
- popper_esm: z.string().url(),
- popper_hash: z.string()
+ floating_ui: z.string().url(),
+ floating_ui_esm: z.string().url(),
+ floating_ui_hash: z.string()
}),
current_version: zVersionSemver,
current_ruby_version: zVersionSemver,