export { default as Datepicker } from './src/datepicker.js'
export { default as Dialog } from './src/dialog.js'
export { default as Dropdown } from './src/dropdown.js'
+export { default as NavOverflow } from './src/nav-overflow.js'
export { default as Offcanvas } from './src/offcanvas.js'
export { default as Strength } from './src/strength.js'
export { default as OtpInput } from './src/otp-input.js'
import Datepicker from './src/datepicker.js'
import Dialog from './src/dialog.js'
import Dropdown from './src/dropdown.js'
+import NavOverflow from './src/nav-overflow.js'
import Offcanvas from './src/offcanvas.js'
import Strength from './src/strength.js'
import OtpInput from './src/otp-input.js'
Datepicker,
Dialog,
Dropdown,
+ NavOverflow,
Offcanvas,
Strength,
OtpInput,
const Default = {
autoClose: true,
boundary: 'clippingParents',
+ container: false,
display: 'dynamic',
offset: [0, 2],
floatingConfig: null,
const DefaultType = {
autoClose: '(boolean|string)',
boundary: '(string|element)',
+ container: '(string|element|boolean)',
display: 'string',
offset: '(array|string|function)',
floatingConfig: '(null|object|function)',
SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||
SelectorEngine.findOne(SELECTOR_MENU, this._parent)
+ // Store original menu parent for container option
+ this._menuOriginalParent = this._menu?.parentNode
+
// Parse responsive placements on init
this._parseResponsivePlacements()
return
}
+ // Move menu to container if specified (to escape overflow clipping)
+ this._moveMenuToContainer()
+
this._createFloating()
// If this is a touch-enabled device we add extra
dispose() {
this._disposeFloating()
+ this._restoreMenuToOriginalParent()
this._disposeMediaQueryListeners()
this._closeAllSubmenus()
this._clearAllSubmenuTimeouts()
this._disposeFloating()
+ // Restore menu to original parent if it was moved
+ this._restoreMenuToOriginalParent()
+
this._menu.classList.remove(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOW)
this._parent.classList.remove(CLASS_NAME_SHOW)
}
}
+ _getContainer() {
+ const { container } = this._config
+ if (container === false) {
+ return null
+ }
+
+ return container === true ? document.body : getElement(container)
+ }
+
+ _moveMenuToContainer() {
+ const container = this._getContainer()
+ if (!container || !this._menu) {
+ return
+ }
+
+ // Only move if not already in the container
+ if (this._menu.parentNode !== container) {
+ container.append(this._menu)
+ }
+ }
+
+ _restoreMenuToOriginalParent() {
+ if (!this._menuOriginalParent || !this._menu) {
+ return
+ }
+
+ // Only restore if menu was moved
+ if (this._menu.parentNode !== this._menuOriginalParent) {
+ this._menuOriginalParent.append(this._menu)
+ }
+ }
+
// Shared helper for positioning any floating element
async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') {
if (!floating.isConnected) {
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
-import Dropdown from './dropdown.js'
/**
* Constants
const overflowItem = document.createElement('li')
overflowItem.className = 'nav-item nav-overflow-item dropdown'
overflowItem.innerHTML = `
- <button class="nav-link nav-overflow-toggle dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+ <button class="nav-link nav-overflow-toggle dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-container="body" data-bs-strategy="fixed" aria-expanded="false">
<span class="nav-overflow-icon">${this._config.moreIcon}</span>
<span class="nav-overflow-text">${this._config.moreText}</span>
</button>
this._element.append(overflowItem)
this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE)
this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU)
-
- // Initialize dropdown with fixed strategy to escape overflow containers
- Dropdown.getOrCreateInstance(this._overflowToggle, {
- strategy: 'fixed'
- })
}
_setupResizeObserver() {
}, 10)
})
})
+
+ it('should move menu to body when container is set to body', () => {
+ 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"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const dropdown = new Dropdown(btnDropdown, {
+ container: 'body'
+ })
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdownMenu.parentNode).toEqual(document.body)
+ resolve()
+ })
+
+ dropdown.show()
+ })
+ })
+
+ it('should move menu to body when container is set to true', () => {
+ 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"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const dropdown = new Dropdown(btnDropdown, {
+ container: true
+ })
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdownMenu.parentNode).toEqual(document.body)
+ resolve()
+ })
+
+ dropdown.show()
+ })
+ })
+
+ it('should move menu to specified element when container is an element', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<div id="custom-container"></div>',
+ '<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"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const customContainer = fixtureEl.querySelector('#custom-container')
+ const dropdown = new Dropdown(btnDropdown, {
+ container: customContainer
+ })
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdownMenu.parentNode).toEqual(customContainer)
+ resolve()
+ })
+
+ dropdown.show()
+ })
+ })
+
+ it('should restore menu to original parent when hidden', () => {
+ 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"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const originalParent = dropdownMenu.parentNode
+ const dropdown = new Dropdown(btnDropdown, {
+ container: 'body'
+ })
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdownMenu.parentNode).toEqual(document.body)
+ dropdown.hide()
+ })
+
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
+ expect(dropdownMenu.parentNode).toEqual(originalParent)
+ resolve()
+ })
+
+ dropdown.show()
+ })
+ })
+
+ it('should work with container via data attribute', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-container="body">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 dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const dropdown = new Dropdown(btnDropdown)
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdownMenu.parentNode).toEqual(document.body)
+ resolve()
+ })
+
+ dropdown.show()
+ })
+ })
})
describe('hide', () => {
--- /dev/null
+import NavOverflow from '../../src/nav-overflow.js'
+import { clearFixture, getFixture } from '../helpers/fixture.js'
+
+describe('NavOverflow', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ describe('VERSION', () => {
+ it('should return plugin version', () => {
+ expect(NavOverflow.VERSION).toEqual(jasmine.any(String))
+ })
+ })
+
+ describe('Default', () => {
+ it('should return plugin default config', () => {
+ expect(NavOverflow.Default).toEqual(jasmine.any(Object))
+ expect(NavOverflow.Default.moreText).toEqual('More')
+ expect(NavOverflow.Default.threshold).toEqual(0)
+ })
+ })
+
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(NavOverflow.DATA_KEY).toEqual('bs.navoverflow')
+ })
+ })
+
+ describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 2</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navBySelector = new NavOverflow('[data-bs-toggle="nav-overflow"]')
+ const navByElement = new NavOverflow(navEl)
+
+ expect(navBySelector._element).toEqual(navEl)
+ expect(navByElement._element).toEqual(navEl)
+
+ navBySelector.dispose()
+ navByElement.dispose()
+ })
+
+ it('should add nav-overflow class to element', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+
+ expect(navEl).toHaveClass('nav-overflow')
+
+ navOverflow.dispose()
+ })
+
+ it('should create overflow menu toggle and dropdown', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+
+ const toggle = navEl.querySelector('.nav-overflow-toggle')
+ const menu = navEl.querySelector('.nav-overflow-menu')
+
+ expect(toggle).not.toBeNull()
+ expect(menu).not.toBeNull()
+ expect(toggle).toHaveClass('dropdown-toggle')
+ expect(menu).toHaveClass('dropdown-menu')
+
+ navOverflow.dispose()
+ })
+
+ it('should store order data on nav items', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 2</a></li>',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 3</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+ const items = navEl.querySelectorAll('.nav-item:not(.nav-overflow-item)')
+
+ expect(items[0].dataset.bsNavOrder).toEqual('0')
+ expect(items[1].dataset.bsNavOrder).toEqual('1')
+ expect(items[2].dataset.bsNavOrder).toEqual('2')
+
+ navOverflow.dispose()
+ })
+
+ it('should respect custom moreText option', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl, {
+ moreText: 'See all'
+ })
+
+ const toggleText = navEl.querySelector('.nav-overflow-text')
+ expect(toggleText.textContent).toEqual('See all')
+
+ navOverflow.dispose()
+ })
+ })
+
+ describe('update', () => {
+ it('should trigger update event', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+
+ navEl.addEventListener('update.bs.navoverflow', () => {
+ navOverflow.dispose()
+ resolve()
+ })
+
+ navOverflow.update()
+ })
+ })
+ })
+
+ describe('dispose', () => {
+ it('should dispose nav overflow and remove overflow menu', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+
+ expect(NavOverflow.getInstance(navEl)).not.toBeNull()
+ expect(navEl.querySelector('.nav-overflow-toggle')).not.toBeNull()
+
+ navOverflow.dispose()
+
+ expect(NavOverflow.getInstance(navEl)).toBeNull()
+ expect(navEl.querySelector('.nav-overflow-toggle')).toBeNull()
+ })
+ })
+
+ describe('getInstance', () => {
+ it('should return nav overflow instance', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+
+ expect(NavOverflow.getInstance(navEl)).toEqual(navOverflow)
+ expect(NavOverflow.getInstance(navEl)).toBeInstanceOf(NavOverflow)
+
+ navOverflow.dispose()
+ })
+
+ it('should return null when there is no instance', () => {
+ fixtureEl.innerHTML = '<ul class="nav"></ul>'
+
+ const navEl = fixtureEl.querySelector('.nav')
+
+ expect(NavOverflow.getInstance(navEl)).toBeNull()
+ })
+ })
+
+ describe('getOrCreateInstance', () => {
+ it('should return nav overflow instance', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+
+ expect(NavOverflow.getOrCreateInstance(navEl)).toEqual(navOverflow)
+ expect(NavOverflow.getInstance(navEl)).toEqual(NavOverflow.getOrCreateInstance(navEl, {}))
+ expect(NavOverflow.getOrCreateInstance(navEl)).toBeInstanceOf(NavOverflow)
+
+ navOverflow.dispose()
+ })
+
+ it('should return new instance when there is no instance', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('.nav')
+
+ expect(NavOverflow.getInstance(navEl)).toBeNull()
+
+ const instance = NavOverflow.getOrCreateInstance(navEl)
+ expect(instance).toBeInstanceOf(NavOverflow)
+
+ instance.dispose()
+ })
+ })
+
+ describe('overflow behavior', () => {
+ it('should use dropdown with container option for overflow menu', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+
+ const toggle = navEl.querySelector('.nav-overflow-toggle')
+ expect(toggle.getAttribute('data-bs-container')).toEqual('body')
+ expect(toggle.getAttribute('data-bs-strategy')).toEqual('fixed')
+
+ navOverflow.dispose()
+ })
+
+ it('should preserve nav-overflow-keep items', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" style="width: 100px;" data-bs-toggle="nav-overflow">',
+ ' <li class="nav-item nav-overflow-keep"><a class="nav-link" href="#">Keep</a></li>',
+ ' <li class="nav-item"><a class="nav-link" href="#">Link 2</a></li>',
+ '</ul>'
+ ].join('')
+
+ const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
+ const navOverflow = new NavOverflow(navEl)
+ const keepItem = navEl.querySelector('.nav-overflow-keep')
+
+ // The keep item should never be hidden
+ expect(keepItem).not.toHaveClass('d-none')
+
+ navOverflow.dispose()
+ })
+ })
+})
@layer components {
.nav-overflow {
flex-wrap: nowrap;
+ min-width: 0; // Allow flex child to shrink below content width
}
// Container item for overflow
$navbar-brand-padding-y: $navbar-brand-height * .5 !default;
$navbar-brand-margin-end: 1rem !default;
+$navbar-toggler-width: 2rem !default;
$navbar-toggler-padding-y: .375rem !default;
$navbar-toggler-padding-x: .375rem !default;
$navbar-toggler-font-size: $font-size-lg !default;
--navbar-brand-color: #{$navbar-light-brand-color};
--navbar-brand-hover-color: #{$navbar-light-brand-hover-color};
--navbar-nav-link-padding-x: #{$navbar-nav-link-padding-x};
+ --navbar-toggler-width: #{$navbar-toggler-width};
--navbar-toggler-padding-y: #{$navbar-toggler-padding-y};
--navbar-toggler-padding-x: #{$navbar-toggler-padding-x};
--navbar-toggler-font-size: #{$navbar-toggler-font-size};
align-items: center;
justify-content: space-between;
padding: var(--navbar-padding-y) var(--navbar-padding-x);
- container-type: inline-size; // Enable container queries for responsive behavior
+ @include set-container();
@include gradient-bg();
// Container properties for nested containers
// Button for toggling the navbar when in its collapsed state
.navbar-toggler {
- padding: var(--navbar-toggler-padding-y) var(--navbar-toggler-padding-x);
- font-size: var(--navbar-toggler-font-size);
- line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--navbar-toggler-width);
+ aspect-ratio: 1 / 1;
+ // padding: var(--navbar-toggler-padding-y) var(--navbar-toggler-padding-x);
+ // font-size: var(--navbar-toggler-font-size);
+ // line-height: 1;
color: var(--navbar-color);
background-color: transparent;
- border: var(--border-width) solid var(--navbar-toggler-border-color);
+ border: 0;
+ // border: var(--border-width) solid var(--navbar-toggler-border-color);
@include border-radius(var(--navbar-toggler-border-radius));
@include transition(var(--navbar-toggler-transition));
// Navbar toggler icon (inline SVG)
.navbar-toggler-icon {
display: inline-block;
- width: 1em;
- height: 1em;
+ width: 1rem;
+ height: 1rem;
color: var(--navbar-color);
- vertical-align: -.125em;
}
.offcanvas-body {
display: flex;
flex-grow: 0;
+ flex-direction: row;
align-items: center;
padding: 0;
overflow-y: visible;
@each $breakpoint in map.keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
- $min-width: breakpoint-min($next, $grid-breakpoints);
- @if $next and $min-width {
+ @if $next {
.navbar-expand#{$infix} {
- @container (min-width: #{$min-width}) {
+ @include container-breakpoint-up($next) {
@include navbar-expanded();
}
}
}
.offcanvas-body {
- display: flex;
flex-grow: 0;
+ flex-direction: row;
padding: 0;
overflow-y: visible;
background-color: transparent !important; // stylelint-disable-line declaration-no-important
// Scrollable body
.offcanvas-body {
+ display: flex;
flex-grow: 1;
+ flex-direction: column;
+ gap: var(--offcanvas-padding-y);
padding: var(--offcanvas-padding-y) var(--offcanvas-padding-x);
overflow-y: auto;
}
h5,
h6 {
&:not(:first-child) {
- margin-top: calc(var(--content-gap) * 1.25);
+ margin-top: var(--content-gap);
}
}
@use "../colors" as *;
@use "../variables" as *;
-$control-min-height: 2.5rem !default;
+$control-min-height: 2.25rem !default;
$control-min-height-sm: 2rem !default;
$control-min-height-lg: 3rem !default;
$control-padding-y: .375rem !default;
}
}
}
+
+
+// Container queries
+//
+// Container queries allow elements to respond to the size of a containing element
+// rather than the viewport. These mixins mirror the media-breakpoint-* mixins above.
+//
+// scss-docs-start container-query-mixins
+
+// Set an element as a query container.
+//
+// @include set-container(); // container-type: inline-size
+// @include set-container(size); // container-type: size
+// @include set-container(inline-size, sidebar); // container: sidebar / inline-size
+//
+@mixin set-container($type: inline-size, $name: null) {
+ @if $name {
+ container: #{$name} / #{$type};
+ } @else {
+ container-type: #{$type};
+ }
+}
+
+// Container query of at least the minimum breakpoint width. No query for the smallest breakpoint.
+// Makes the @content apply to the given breakpoint and wider within the container.
+//
+// @include container-breakpoint-up(md) { ... }
+// @include container-breakpoint-up(lg, sidebar) { ... } // Query named container
+//
+@mixin container-breakpoint-up($name, $container-name: null, $breakpoints: $grid-breakpoints) {
+ $min: breakpoint-min($name, $breakpoints);
+ @if $min {
+ @if $container-name {
+ @container #{$container-name} (width >= #{$min}) {
+ @content;
+ }
+ } @else {
+ @container (width >= #{$min}) {
+ @content;
+ }
+ }
+ } @else {
+ @content;
+ }
+}
+
+// Container query of at most the maximum breakpoint width. No query for the largest breakpoint.
+// Makes the @content apply to the given breakpoint and narrower within the container.
+//
+// @include container-breakpoint-down(lg) { ... }
+// @include container-breakpoint-down(lg, sidebar) { ... } // Query named container
+//
+@mixin container-breakpoint-down($name, $container-name: null, $breakpoints: $grid-breakpoints) {
+ $max: breakpoint-max($name, $breakpoints);
+ @if $max {
+ @if $container-name {
+ @container #{$container-name} (width < #{$max}) {
+ @content;
+ }
+ } @else {
+ @container (width < #{$max}) {
+ @content;
+ }
+ }
+ } @else {
+ @content;
+ }
+}
+
+// Container query that spans multiple breakpoint widths.
+// Makes the @content apply between the min and max breakpoints within the container.
+//
+// @include container-breakpoint-between(md, xl) { ... }
+// @include container-breakpoint-between(md, xl, sidebar) { ... } // Query named container
+//
+@mixin container-breakpoint-between($lower, $upper, $container-name: null, $breakpoints: $grid-breakpoints) {
+ $min: breakpoint-min($lower, $breakpoints);
+ $max: breakpoint-max($upper, $breakpoints);
+
+ @if $min != null and $max != null {
+ @if $container-name {
+ @container #{$container-name} (width >= #{$min}) and (width < #{$max}) {
+ @content;
+ }
+ } @else {
+ @container (width >= #{$min}) and (width < #{$max}) {
+ @content;
+ }
+ }
+ } @else if $max == null {
+ @include container-breakpoint-up($lower, $container-name, $breakpoints) {
+ @content;
+ }
+ } @else if $min == null {
+ @include container-breakpoint-down($upper, $container-name, $breakpoints) {
+ @content;
+ }
+ }
+}
+
+// Container query between the breakpoint's minimum and maximum widths.
+// No minimum for the smallest breakpoint, and no maximum for the largest one.
+// Makes the @content apply only to the given breakpoint within the container.
+//
+// @include container-breakpoint-only(md) { ... }
+// @include container-breakpoint-only(md, sidebar) { ... } // Query named container
+//
+@mixin container-breakpoint-only($name, $container-name: null, $breakpoints: $grid-breakpoints) {
+ $min: breakpoint-min($name, $breakpoints);
+ $next: breakpoint-next($name, $breakpoints);
+ $max: breakpoint-max($next, $breakpoints);
+
+ @if $min != null and $max != null {
+ @if $container-name {
+ @container #{$container-name} (width >= #{$min}) and (width < #{$max}) {
+ @content;
+ }
+ } @else {
+ @container (width >= #{$min}) and (width < #{$max}) {
+ @content;
+ }
+ }
+ } @else if $max == null {
+ @include container-breakpoint-up($name, $container-name, $breakpoints) {
+ @content;
+ }
+ } @else if $min == null {
+ @include container-breakpoint-down($next, $container-name, $breakpoints) {
+ @content;
+ }
+ }
+}
+// scss-docs-end container-query-mixins
| --- | --- | --- | --- |
| `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 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). |
+| `container` | string, element, boolean | `false` | Appends the dropdown menu to a specific element when shown. Use `'body'` or `true` to append to the document body, which helps escape containers with `overflow: hidden`. The menu is moved back to its original position when hidden. |
| `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. |
<ResizableExample code={`<nav class="navbar navbar-expand bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Brand</a>
- <ul class="navbar-nav" data-bs-toggle="nav-overflow">
+ <ul class="nav navbar-nav" data-bs-toggle="nav-overflow">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
Here's what you need to know before getting started with the navbar:
-- Navbars require a wrapping `.navbar` with `.navbar-expand-lg{-sm|-md|-lg|-xl|-2xl}` for responsive collapsing and [color scheme](#color-schemes) classes.
+- Navbars require a wrapping `.navbar` with `.navbar-expand-{breakpoint}` for responsive collapsing and [color scheme](#color-schemes) classes.
- Navbars and their contents are fluid by default. Change the [container](#containers) to limit their horizontal width in different ways.
- Use our [margin]([[docsref:/utilities/margin]]), [padding]([[docsref:/utilities/padding]]), and [flex]([[docsref:/utilities/flex]]) utility classes for controlling spacing and alignment within navbars.
- Navbars are responsive by default using our **offcanvas component**. On mobile, navigation links slide in from the side as a drawer.
<CloseButton dismiss="offcanvas" />
</div>
<div class="offcanvas-body mb-2 mb-md-0">
- <ul class="nav nav-pills me-auto">
+ <ul class="nav navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
// Apply styles starting from medium devices and up to extra large devices
@media (width >= 768px) and (width < 1200px) { ... }
```
+
+## Container queries
+
+In addition to viewport-based media queries, Bootstrap uses [container queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries) for certain components. Container queries allow elements to respond to the size of a parent element rather than the viewport, enabling more flexible and modular responsive behavior.
+
+Container queries use the same breakpoint values as viewport media queries.
+
+### Usage in Bootstrap
+
+The following components use container queries:
+
+- **[Navbar]([[docsref:/components/navbar]])** — Uses container queries for its responsive expand behavior. The `.navbar` element is set as a query container, and the `.navbar-expand-*` classes use `@container` queries instead of `@media` queries. This means the navbar responds to its own width rather than the viewport width, making it more adaptable when placed in different layout contexts (e.g., within a sidebar or constrained container).
+
+- **[Stepper]([[docsref:/components/stepper]])** — The `.stepper-overflow` wrapper uses `container-type: inline-size` to establish a containment context for the horizontally scrolling stepper pattern.
+
+Here's how the navbar implements container queries:
+
+```scss
+// The navbar is defined as a query container
+.navbar {
+ container-type: inline-size;
+}
+
+// Responsive classes use container queries
+.navbar-expand-lg {
+ @container (min-width: 1024px) {
+ // Expanded navbar styles...
+ }
+}
+```
+
+### Setting a container
+
+Use the `set-container()` mixin to establish an element as a query container:
+
+```scss
+// Default: inline-size containment
+.my-component {
+ @include set-container();
+ // Output: container-type: inline-size;
+}
+
+// With explicit type
+.my-component {
+ @include set-container(size);
+ // Output: container-type: size;
+}
+
+// With a name (uses shorthand property)
+.my-component {
+ @include set-container(inline-size, sidebar);
+ // Output: container: sidebar / inline-size;
+}
+```
+
+### Container query mixins
+
+Similar to the `media-breakpoint-*` mixins for viewport-based media queries, Bootstrap provides container query mixins that use the same breakpoint values and range syntax.
+
+#### Min-width
+
+Use `container-breakpoint-up()` to apply styles when the container is at least the given breakpoint width:
+
+```scss
+.my-component {
+ @include set-container();
+}
+
+.my-component-child {
+ // …
+
+ @include container-breakpoint-up(md) {
+ // Styles for when container is at least 768px wide
+ }
+
+ @include container-breakpoint-up(lg) {
+ // Styles for when container is at least 1024px wide
+ }
+}
+```
+
+These mixins use the same modern range syntax as media queries. For example:
+
+```scss
+@container (width >= 768px) { ... }
+@container (width >= 1024px) { ... }
+```
+
+#### Max-width
+
+Use `container-breakpoint-down()` to apply styles when the container is narrower than the given breakpoint:
+
+```scss
+// Sass usage
+@include container-breakpoint-down(lg) { ... }
+
+// Compiled CSS
+@container (width < 1024px) { ... }
+```
+
+#### Single breakpoint
+
+Use `container-breakpoint-only()` to target a single breakpoint range:
+
+```scss
+// Sass usage
+@include container-breakpoint-only(md) { ... }
+
+// Compiled CSS
+@container (width >= 768px) and (width < 1024px) { ... }
+```
+
+#### Between breakpoints
+
+Use `container-breakpoint-between()` to span multiple breakpoint widths:
+
+```scss
+// Sass usage
+@include container-breakpoint-between(md, xl) { ... }
+
+// Compiled CSS
+@container (width >= 768px) and (width < 1280px) { ... }
+```
+
+### Named containers
+
+All container query mixins accept an optional container name parameter for querying a specific ancestor container. This is useful when you have nested containers:
+
+```scss
+.sidebar {
+ @include set-container(inline-size, sidebar);
+}
+
+.main-content {
+ @include set-container(inline-size, main);
+}
+
+// Query the sidebar container specifically, even if nested inside main
+.widget {
+ @include container-breakpoint-up(md, sidebar) {
+ // …
+ }
+}
+```
+
+The compiled output includes the container name:
+
+```scss
+@container sidebar (width >= 768px) { ... }
+```
}
.bd-subtitle {
- font-size: 1.5rem;
+ font-size: var(--font-size-lg);
font-weight: 300;
}