Bootstrap v6 — ESLint Modernization Report

Outdated JS patterns that can be replaced with modern alternatives

78 Warnings
14 Rules
20 Files affected

Prefer globalThis over window

unicorn/prefer-global-this auto-fixable 15

globalThis is the standard way to access the global object across all JS environments (browser, Node, workers, Deno). window only works in browsers and fails in SSR/testing contexts.

MDN Documentation →
Before (current)
window.addEventListener(...)
After (modern)
globalThis.addEventListener(...)
FileLineCurrent codeSuggested fix
js/src/carousel.js 437:17 EventHandler.on(window, EVENT_LOAD_DATA_API, () => { EventHandler.on(globalThis, EVENT_LOAD_DATA_API, () => {
js/src/chip-input.js 603:48 const pastedData = (event.clipboardData || window.clipboardData).getData('text') const pastedData = (event.clipboardData || globalThis.clipboardData).getData('text')
js/src/offcanvas.js 242:17 EventHandler.on(window, EVENT_LOAD_DATA_API, () => { EventHandler.on(globalThis, EVENT_LOAD_DATA_API, () => {
js/src/offcanvas.js 248:17 EventHandler.on(window, EVENT_RESIZE, () => { EventHandler.on(globalThis, EVENT_RESIZE, () => {
js/src/otp-input.js 212:48 const pastedData = (event.clipboardData || window.clipboardData).getData('text') const pastedData = (event.clipboardData || globalThis.clipboardData).getData('text')
js/src/scrollspy.js 133:43 const root = this._rootElement || window const root = this._rootElement || globalThis
js/src/scrollspy.js 261:17 EventHandler.on(window, EVENT_LOAD_DATA_API, () => { EventHandler.on(globalThis, EVENT_LOAD_DATA_API, () => {
js/src/tab.js 287:17 EventHandler.on(window, EVENT_LOAD_DATA_API, () => { EventHandler.on(globalThis, EVENT_LOAD_DATA_API, () => {
js/src/util/floating-ui.js 110:17 const mql = window.matchMedia(`(min-width: ${minWidth}px)`) const mql = globalThis.matchMedia(`(min-width: ${minWidth}px)`)
js/src/util/index.js 18:19 if (selector && window.CSS && window.CSS.escape) { if (selector && globalThis.CSS && window.CSS.escape) {
js/src/util/index.js 18:33 if (selector && window.CSS && window.CSS.escape) { if (selector && window.CSS && globalThis.CSS.escape) {
js/src/util/index.js 53:49 let { transitionDuration, transitionDelay } = window.getComputedStyle(element) let { transitionDuration, transitionDelay } = globalThis.getComputedStyle(element)
js/src/util/sanitizer.js 93:25 const domParser = new window.DOMParser() const domParser = new globalThis.DOMParser()
js/src/util/scrollbar.js 72:31 const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty) const calculatedValue = globalThis.getComputedStyle(element).getPropertyValue(styleProperty)
js/src/util/swipe.js 55:42 this._supportPointerEvents = Boolean(window.PointerEvent) this._supportPointerEvents = Boolean(globalThis.PointerEvent)

Prefer spread operator over Array.from / concat / split

unicorn/prefer-spread auto-fixable 12

The spread operator [...iterable] is more readable and often faster than Array.from(), [].concat(), or String#split("").

MDN Documentation →
Before (current)
[].concat(...arrays)
Array.from(nodeList)
str.split("")
After (modern)
[...arrays].flat()
[...nodeList]
[...str]
FileLineCurrent codeSuggested fix
js/src/dom/data.js 26:100 console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`) console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...instanceMap.keys()][0]}.`)
js/src/dom/selector-engine.js 37:15 return [].concat(...Element.prototype.querySelectorAll.call(element, selector))
js/src/dom/selector-engine.js 45:15 return [].concat(...element.children).filter(child => child.matches(selector))
js/src/dropdown.js 195:32 for (const element of [].concat(...document.body.children)) {
js/src/dropdown.js 248:32 for (const element of [].concat(...document.body.children)) {
js/src/otp-input.js 69:33 const chars = String(value).split('')
js/src/otp-input.js 139:27 const chars = value.split('')
js/src/tooltip.js 241:32 for (const element of [].concat(...document.body.children)) {
js/src/tooltip.js 275:32 for (const element of [].concat(...document.body.children)) {
js/src/util/sanitizer.js 95:23 const elements = [].concat(...createdDocument.body.querySelectorAll('*'))
js/src/util/sanitizer.js 105:30 const attributeList = [].concat(...element.attributes)
js/src/util/sanitizer.js 106:34 const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])

Prefer .dataset over getAttribute/setAttribute for data-* attributes

unicorn/prefer-dom-node-dataset auto-fixable 9

element.dataset provides a clean API for data-* attributes. No need for manual string manipulation with getAttribute("data-...").

MDN Documentation →
Before (current)
el.getAttribute("data-bs-interval")
el.setAttribute("data-bs-foo", val)
After (modern)
el.dataset.bsInterval
el.dataset.bsFoo = val
FileLineCurrent codeSuggested fix
js/src/carousel.js 293:45 const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10) const elementInterval = Number.parseInt(element.dataset.bsInterval, 10)
js/src/carousel.js 419:22 const slideIndex = this.getAttribute('data-bs-slide-to') const slideIndex = this.dataset.bsSlideTo
js/src/datepicker.js 280:7 element.setAttribute('data-bs-theme', theme) element.dataset.bsTheme = theme
js/src/datepicker.js 283:7 element.removeAttribute('data-bs-theme') delete element.dataset.bsTheme
js/src/dom/selector-engine.js 11:18 let selector = element.getAttribute('data-bs-target') let selector = element.dataset.bsTarget
js/src/tooltip.js 193:9 if (this._element.getAttribute('data-bs-original-title')) { if (this._element.dataset.bsOriginalTitle) {
js/src/tooltip.js 194:43 this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')) this._element.setAttribute('title', this._element.dataset.bsOriginalTitle)
js/src/tooltip.js 368:65 return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title') return this._resolvePossibleFunction(this._config.title) || this._element.dataset.bsOriginalTitle
js/src/tooltip.js 609:5 this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility this._element.dataset.bsOriginalTitle = title // DO NOT USE IT. Is only for backwards compatibility

Prefer Array#flat() over [].concat() tricks

unicorn/prefer-array-flat auto-fixable 8

Array#flat() is the standard, readable way to flatten arrays. The old [].concat(...arr) pattern was a workaround before flat() existed.

MDN Documentation →
Before (current)
[].concat(...nestedArray)
After (modern)
nestedArray.flat()
FileLineCurrent codeSuggested fix
js/src/dom/selector-engine.js 37:12 return [].concat(...Element.prototype.querySelectorAll.call(element, selector)) return Element.prototype.querySelectorAll.call(element, selector).flat()
js/src/dom/selector-engine.js 45:12 return [].concat(...element.children).filter(child => child.matches(selector)) return element.children.flat().filter(child => child.matches(selector))
js/src/dropdown.js 195:29 for (const element of [].concat(...document.body.children)) { for (const element of document.body.children.flat()) {
js/src/dropdown.js 248:29 for (const element of [].concat(...document.body.children)) { for (const element of document.body.children.flat()) {
js/src/tooltip.js 241:29 for (const element of [].concat(...document.body.children)) { for (const element of document.body.children.flat()) {
js/src/tooltip.js 275:29 for (const element of [].concat(...document.body.children)) { for (const element of document.body.children.flat()) {
js/src/util/sanitizer.js 95:20 const elements = [].concat(...createdDocument.body.querySelectorAll('*')) const elements = createdDocument.body.querySelectorAll('*').flat()
js/src/util/sanitizer.js 105:27 const attributeList = [].concat(...element.attributes) const attributeList = element.attributes.flat()

Use destructured variables consistently

unicorn/consistent-destructuring manual fix 7

If a variable was destructured from an object, always use the destructured variable instead of accessing the property again on the original object.

MDN Documentation →
Before (current)
const { foo } = obj
console.log(obj.bar)
After (modern)
const { foo, bar } = obj
console.log(bar)
FileLineCurrent code
js/src/chip-input.js 439:17 if (event.shiftKey) {
js/src/chip-input.js 465:18 const chip = event.target.closest(SELECTOR_CHIP)
js/src/chip-input.js 483:53 this._navigateChip(chips, currentIndex, -1, event.shiftKey)
js/src/chip-input.js 489:52 this._navigateChip(chips, currentIndex, 1, event.shiftKey)
js/src/chip-input.js 495:40 this._navigateToEdge(chips, 0, event.shiftKey)
js/src/strength.js 150:28 if (password.length >= this._config.minLength) {
js/src/strength.js 154:28 if (password.length >= this._config.minLength + 4) {

Use logical assignment operators (||=, &&=, ??=)

logical-assignment-operators auto-fixable 6

Logical assignment operators combine a logical operation with assignment in a single, concise expression. Reduces boilerplate and makes intent clearer.

MDN Documentation →
Before (current)
x = x || defaultValue
if (!x) { x = fallback }
After (modern)
x ||= defaultValue
x ||= fallback
FileLineCurrent codeSuggested fix
js/src/dom/event-handler.js 83:3 eventRegistry[uid] = eventRegistry[uid] || {}
js/src/dom/event-handler.js 161:20 const handlers = events[typeEvent] || (events[typeEvent] = {}) const handlers = (events[typeEvent] ||= {})
js/src/dom/event-handler.js 165:5 previousFunction.oneOff = previousFunction.oneOff && oneOff
js/src/tooltip.js 313:5 if (!this.tip) { this.tip ||= this._createTipElement(this._newContent || this._getContentForTemplate());
js/src/tooltip.js 444:5 if (!placement) { placement ||= this._getPlacement(tip);
js/src/tooltip.js 448:5 if (!arrowElement) { arrowElement ||= tip.querySelector(`.${this.constructor.NAME}-arrow`);

Avoid typeof check for undefined

unicorn/no-typeof-undefined auto-fixable 5

typeof x !== "undefined" is a legacy pattern. Since ES5, undefined is non-writable on the global object, so direct comparison x !== undefined is safe and shorter.

MDN Documentation →
Before (current)
if (typeof x !== "undefined") { ... }
After (modern)
if (x !== undefined) { ... }
FileLineCurrent codeSuggested fix
js/src/dom/event-handler.js 231:9 if (typeof callable !== 'undefined') { if (callable !== undefined) {
js/src/dropdown.js 128:9 if (typeof computePosition === 'undefined') { if (computePosition === undefined) {
js/src/tooltip.js 121:9 if (typeof computePosition === 'undefined') { if (computePosition === undefined) {
js/src/util/index.js 79:10 return typeof object.nodeType !== 'undefined' return object.nodeType !== undefined
js/src/util/index.js 130:7 if (typeof element.disabled !== 'undefined') { if (element.disabled !== undefined) {

TODO comments flagged for v6

no-warning-comments manual fix 5

These are action items left by the Bootstrap team specifically for the v6 release. They should be resolved now.

Before (current)
// TODO: v6 revert #37011 & change markup
After (modern)
(resolve the TODO — implement the change)
FileLineCurrent code
js/src/dom/selector-engine.js 74:3 // TODO: this is now unused; remove later along with prev()
js/src/dropdown.js 143:5 // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
js/src/dropdown.js 922:5 // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
js/src/tab.js 48:105 const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]' // TODO: could only be `tab` in v6
js/src/tab.js 64:7 // TODO: should throw exception in v6

Prefer String#replaceAll() over regex replace

unicorn/prefer-string-replace-all auto-fixable 4

replaceAll() is clearer than replace(/pattern/g, ...) for simple string replacements. No need to escape regex special characters.

MDN Documentation →
Before (current)
str.replace(/foo/g, "bar")
After (modern)
str.replaceAll("foo", "bar")
FileLineCurrent codeSuggested fix
js/src/dom/manipulator.js 37:14 return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`) return key.replaceAll(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)
js/src/otp-input.js 131:33 input.value = input.value.replace(/\D/g, '') input.value = input.value.replaceAll(/\D/g, '')
js/src/otp-input.js 213:31 const digits = pastedData.replace(/\D/g, '').slice(0, this._inputs.length) const digits = pastedData.replaceAll(/\D/g, '').slice(0, this._inputs.length)
js/src/util/index.js 20:25 selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`) selector = selector.replaceAll(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`)

Prefer .at() for index access from end

unicorn/prefer-at auto-fixable 2

Array#at(-1) is the modern way to access elements from the end. More readable than arr[arr.length - 1].

MDN Documentation →
Before (current)
items[items.length - 1]
After (modern)
items.at(-1)
FileLineCurrent codeSuggested fix
js/src/dropdown.js 853:64 const targetItem = key === HOME_KEY ? items[0] : items[items.length - 1] const targetItem = key === HOME_KEY ? items[0] : items.at(-1)
js/src/util/focustrap.js 100:16 elements[elements.length - 1].focus() elements.at(-1).focus()

Use named capture groups in regex

prefer-named-capture-group manual fix 2

Named capture groups (?<name>...) make regex matches self-documenting. Accessing match.groups.name is clearer than match[1].

MDN Documentation →
Before (current)
const match = /^(\d+)-(\w+)$/.exec(str)
After (modern)
const match = /^(?<num>\d+)-(?<id>\w+)$/.exec(str)
FileLineCurrent code
js/src/util/index.js 20:33 selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`)
js/src/util/index.js 32:55 return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase()

High cyclomatic complexity

complexity manual fix 1

Methods with complexity > 20 are hard to test and maintain. Consider breaking into smaller, focused functions.

Before (current)
function bigMethod() { /* 24 branches */ }
After (modern)
function bigMethod() { /* split into helpers */ }
FileLineCurrent code
js/src/dropdown.js 901:31 static dataApiKeydownHandler(event) {

Prefer String.raw for strings with backslashes

unicorn/prefer-string-raw manual fix 1

String.raw avoids double-escaping backslashes in template literals, making regex patterns and file paths more readable.

MDN Documentation →
Before (current)
"\\d+"
After (modern)
String.raw`\d+`
FileLineCurrent codeSuggested fix
js/src/otp-input.js 101:37 input.setAttribute('pattern', '\\d*') input.setAttribute('pattern', String.raw`\d*`)

Prefer querySelector over getElementById

unicorn/prefer-query-selector auto-fixable 1

querySelector is more flexible and consistent with other DOM querying methods. It uses CSS selector syntax uniformly.

MDN Documentation →
Before (current)
document.getElementById("myId")
After (modern)
document.querySelector("#myId")
FileLineCurrent code
js/src/util/index.js 42:21 } while (document.getElementById(prefix))