Outdated JS patterns that can be replaced with modern alternatives
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.
window.addEventListener(...)globalThis.addEventListener(...)| File | Line | Current code | Suggested 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) |
unicorn/prefer-spread
auto-fixable
12
The spread operator [...iterable] is more readable and often faster than Array.from(), [].concat(), or String#split("").
[].concat(...arrays)
Array.from(nodeList)
str.split("")[...arrays].flat()
[...nodeList]
[...str]| File | Line | Current code | Suggested 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] || []) |
— |
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-...").
el.getAttribute("data-bs-interval")
el.setAttribute("data-bs-foo", val)el.dataset.bsInterval
el.dataset.bsFoo = val| File | Line | Current code | Suggested 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 |
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.
[].concat(...nestedArray)nestedArray.flat()| File | Line | Current code | Suggested 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() |
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 →const { foo } = obj
console.log(obj.bar)const { foo, bar } = obj
console.log(bar)| File | Line | Current 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) { |
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 →x = x || defaultValue
if (!x) { x = fallback }x ||= defaultValue
x ||= fallback| File | Line | Current code | Suggested 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`); |
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.
if (typeof x !== "undefined") { ... }if (x !== undefined) { ... }| File | Line | Current code | Suggested 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) { |
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.
// TODO: v6 revert #37011 & change markup(resolve the TODO — implement the change)| File | Line | Current 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 |
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.
str.replace(/foo/g, "bar")str.replaceAll("foo", "bar")| File | Line | Current code | Suggested 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)}`) |
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].
items[items.length - 1]items.at(-1)| File | Line | Current code | Suggested 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() |
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].
const match = /^(\d+)-(\w+)$/.exec(str)const match = /^(?<num>\d+)-(?<id>\w+)$/.exec(str)| File | Line | Current 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() |
complexity
manual fix
1
Methods with complexity > 20 are hard to test and maintain. Consider breaking into smaller, focused functions.
function bigMethod() { /* 24 branches */ }function bigMethod() { /* split into helpers */ }| File | Line | Current code |
|---|---|---|
| js/src/dropdown.js | 901:31 | static dataApiKeydownHandler(event) { |
unicorn/prefer-string-raw
manual fix
1
String.raw avoids double-escaping backslashes in template literals, making regex patterns and file paths more readable.
"\\d+"String.raw`\d+`| File | Line | Current code | Suggested fix |
|---|---|---|---|
| js/src/otp-input.js | 101:37 | input.setAttribute('pattern', '\\d*') |
input.setAttribute('pattern', String.raw`\d*`) |
unicorn/prefer-query-selector
auto-fixable
1
querySelector is more flexible and consistent with other DOM querying methods. It uses CSS selector syntax uniformly.
document.getElementById("myId")document.querySelector("#myId")| File | Line | Current code |
|---|---|---|
| js/src/util/index.js | 42:21 | } while (document.getElementById(prefix)) |