From c9144d725e97f350fdf6fdcb25a4e41419859d58 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Fri, 3 Apr 2026 22:03:39 -0700 Subject: [PATCH 1/8] simplify eslint configs, modernize eslint config for docs js --- .eslintrc.json | 254 ------------------ eslint.config.js | 21 +- js/src/combobox.js | 4 +- js/src/dom/data.js | 2 +- js/src/dom/selector-engine.js | 4 +- js/src/menu.js | 6 +- js/src/otp-input.js | 4 +- js/src/tab.js | 2 +- js/src/tooltip.js | 4 +- js/src/util/focustrap.js | 2 +- js/src/util/sanitizer.js | 6 +- js/tests/integration/bundle-modularity.js | 2 +- js/tests/integration/bundle.js | 2 +- js/tests/unit/collapse.spec.js | 2 +- js/tests/unit/dom/selector-engine.spec.js | 10 +- site/src/assets/examples/sidebars/sidebars.js | 2 +- site/src/assets/stackblitz.js | 2 +- 17 files changed, 33 insertions(+), 296 deletions(-) delete mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index a4ad6bd3473b..000000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,254 +0,0 @@ -{ - "root": true, - "extends": [ - "plugin:import/errors", - "plugin:import/warnings", - "plugin:unicorn/recommended", - "xo", - "xo/browser" - ], - "rules": { - "arrow-body-style": "off", - "capitalized-comments": "off", - "comma-dangle": [ - "error", - "never" - ], - "import/extensions": [ - "error", - "ignorePackages", - { - "js": "always" - } - ], - "import/first": "error", - "import/newline-after-import": "error", - "import/no-absolute-path": "error", - "import/no-amd": "error", - "import/no-cycle": [ - "error", - { - "ignoreExternal": true - } - ], - "import/no-duplicates": "error", - "import/no-extraneous-dependencies": "error", - "import/no-mutable-exports": "error", - "import/no-named-as-default": "error", - "import/no-named-as-default-member": "error", - "import/no-named-default": "error", - "import/no-self-import": "error", - "import/no-unassigned-import": [ - "error" - ], - "import/no-useless-path-segments": "error", - "import/order": "error", - "indent": [ - "error", - 2, - { - "MemberExpression": "off", - "SwitchCase": 1 - } - ], - "logical-assignment-operators": "off", - "max-params": [ - "warn", - 5 - ], - "multiline-ternary": [ - "error", - "always-multiline" - ], - "new-cap": [ - "error", - { - "properties": false - } - ], - "no-console": "error", - "no-negated-condition": "off", - "object-curly-spacing": [ - "error", - "always" - ], - "operator-linebreak": [ - "error", - "after" - ], - "prefer-object-has-own": "off", - "prefer-template": "error", - "semi": [ - "error", - "never" - ], - "strict": "error", - "unicorn/explicit-length-check": "off", - "unicorn/filename-case": "off", - "unicorn/no-anonymous-default-export": "off", - "unicorn/no-array-callback-reference": "off", - "unicorn/no-array-method-this-argument": "off", - "unicorn/no-null": "off", - "unicorn/no-typeof-undefined": "off", - "unicorn/no-unused-properties": "error", - "unicorn/numeric-separators-style": "off", - "unicorn/prefer-array-flat": "off", - "unicorn/prefer-at": "off", - "unicorn/prefer-dom-node-dataset": "off", - "unicorn/prefer-global-this": "off", - "unicorn/prefer-module": "off", - "unicorn/prefer-query-selector": "off", - "unicorn/prefer-spread": "off", - "unicorn/prefer-string-raw": "off", - "unicorn/prefer-string-replace-all": "off", - "unicorn/prefer-structured-clone": "off", - "unicorn/prevent-abbreviations": "off" - }, - "overrides": [ - { - "files": [ - "build/**" - ], - "env": { - "browser": false, - "node": true - }, - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "no-console": "off", - "unicorn/prefer-top-level-await": "off" - } - }, - { - "files": [ - "js/**" - ], - "parserOptions": { - "sourceType": "module" - } - }, - { - "files": [ - "js/tests/*.js", - "js/tests/integration/rollup*.js" - ], - "env": { - "node": true - }, - "parserOptions": { - "sourceType": "script" - } - }, - { - "files": [ - "js/tests/unit/**" - ], - "env": { - "jasmine": true - }, - "rules": { - "no-console": "off", - "unicorn/consistent-function-scoping": "off", - "unicorn/no-useless-undefined": "off", - "unicorn/prefer-add-event-listener": "off" - } - }, - { - "files": [ - "js/tests/visual/**" - ], - "plugins": [ - "html" - ], - "settings": { - "html/html-extensions": [ - ".html" - ] - }, - "rules": { - "no-console": "off", - "no-new": "off", - "unicorn/no-array-for-each": "off" - } - }, - { - "files": [ - "scss/tests/**" - ], - "env": { - "node": true - }, - "parserOptions": { - "sourceType": "script" - } - }, - { - "files": [ - "site/**" - ], - "env": { - "browser": true, - "node": false - }, - "parserOptions": { - "sourceType": "script", - "ecmaVersion": 2019 - }, - "rules": { - "no-new": "off", - "unicorn/no-array-for-each": "off" - } - }, - { - "files": [ - "site/src/assets/application.js", - "site/src/assets/partials/*.js", - "site/src/assets/search.js", - "site/src/assets/snippets.js", - "site/src/assets/stackblitz.js", - "site/src/plugins/*.js" - ], - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2020 - } - }, - { - "files": [ - "site/src/assets/examples/cheatsheet/cheatsheet.js", - "site/src/assets/examples/sidebars/sidebars.js" - ], - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2020 - }, - "rules": { - "import/no-unresolved": "off" - } - }, - { - "files": [ - "**/*.md" - ], - "plugins": [ - "markdown" - ], - "processor": "markdown/markdown" - }, - { - "files": [ - "**/*.md/*.js", - "**/*.md/*.mjs" - ], - "extends": "plugin:markdown/recommended-legacy", - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "unicorn/prefer-node-protocol": "off" - } - } - ] -} diff --git a/eslint.config.js b/eslint.config.js index 02cbed068584..d807078449a1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -95,12 +95,12 @@ const localRules = { 'unicorn/no-unused-properties': 'error', 'unicorn/numeric-separators-style': 'off', 'unicorn/prefer-array-flat': 'off', - 'unicorn/prefer-at': 'off', + 'unicorn/prefer-at': 'error', 'unicorn/prefer-dom-node-dataset': 'off', 'unicorn/prefer-global-this': 'off', 'unicorn/prefer-module': 'off', 'unicorn/prefer-query-selector': 'off', - 'unicorn/prefer-spread': 'off', + 'unicorn/prefer-spread': 'error', 'unicorn/prefer-string-raw': 'off', 'unicorn/prefer-string-replace-all': 'off', 'unicorn/prefer-structured-clone': 'off', @@ -266,17 +266,14 @@ const eslintConfig = [ } }, - // site/** — browser, script mode, older ecmaVersion + // site/** — browser, script mode { files: ['site/**'], languageOptions: { globals: { ...globals.browser }, - sourceType: 'script', - parserOptions: { - ecmaVersion: 2019 - } + sourceType: 'script' }, rules: { 'no-new': 'off', @@ -296,10 +293,7 @@ const eslintConfig = [ 'site/src/plugins/*.js' ], languageOptions: { - sourceType: 'module', - parserOptions: { - ecmaVersion: 2020 - } + sourceType: 'module' }, // These files may have eslint-disable directives for the old import plugin linterOptions: { @@ -322,10 +316,7 @@ const eslintConfig = [ 'site/src/assets/examples/sidebars/sidebars.js' ], languageOptions: { - sourceType: 'module', - parserOptions: { - ecmaVersion: 2020 - } + sourceType: 'module' }, rules: { 'import/no-unresolved': 'off' diff --git a/js/src/combobox.js b/js/src/combobox.js index fad08043b7ed..9076e13a2418 100644 --- a/js/src/combobox.js +++ b/js/src/combobox.js @@ -359,7 +359,7 @@ class Combobox extends BaseComponent { const items = this._getVisibleItems() if (items.length > 0) { - const target = key === ARROW_DOWN_KEY ? items[0] : items[items.length - 1] + const target = key === ARROW_DOWN_KEY ? items[0] : items.at(-1) target.focus() } @@ -404,7 +404,7 @@ class Combobox extends BaseComponent { event.preventDefault() const items = this._getVisibleItems() if (items.length > 0) { - const targetItem = key === HOME_KEY ? items[0] : items[items.length - 1] + const targetItem = key === HOME_KEY ? items[0] : items.at(-1) targetItem.focus() } diff --git a/js/src/dom/data.js b/js/src/dom/data.js index 10eee5e22469..6ab30c9bbe2d 100644 --- a/js/src/dom/data.js +++ b/js/src/dom/data.js @@ -23,7 +23,7 @@ export default { // can be removed later when multiple key/instances are fine to be used if (!instanceMap.has(key) && instanceMap.size !== 0) { // eslint-disable-next-line no-console - 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]}.`) return } diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index d4cee4d811a1..9e83d2064ab2 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -34,7 +34,7 @@ const getSelector = element => { const SelectorEngine = { find(selector, element = document.documentElement) { - return [].concat(...Element.prototype.querySelectorAll.call(element, selector)) + return [...Element.prototype.querySelectorAll.call(element, selector)] }, findOne(selector, element = document.documentElement) { @@ -42,7 +42,7 @@ const SelectorEngine = { }, children(element, selector) { - return [].concat(...element.children).filter(child => child.matches(selector)) + return [...element.children].filter(child => child.matches(selector)) }, parents(element, selector) { diff --git a/js/src/menu.js b/js/src/menu.js index 3689b3fdddf3..f70a13457606 100644 --- a/js/src/menu.js +++ b/js/src/menu.js @@ -186,7 +186,7 @@ class Menu extends BaseComponent { this._createFloating() if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { - for (const element of [].concat(...document.body.children)) { + for (const element of document.body.children) { EventHandler.on(element, 'mouseover', noop) } } @@ -249,7 +249,7 @@ class Menu extends BaseComponent { this._closeAllSubmenus() if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { + for (const element of document.body.children) { EventHandler.off(element, 'mouseover', noop) } } @@ -837,7 +837,7 @@ class Menu extends BaseComponent { .filter(element => isVisible(element)) if (items.length) { - const targetItem = key === HOME_KEY ? items[0] : items[items.length - 1] + const targetItem = key === HOME_KEY ? items[0] : items.at(-1) targetItem.focus() } diff --git a/js/src/otp-input.js b/js/src/otp-input.js index 6641eb39cd02..dffa1d97b31d 100644 --- a/js/src/otp-input.js +++ b/js/src/otp-input.js @@ -66,7 +66,7 @@ class OtpInput extends BaseComponent { } setValue(value) { - const chars = String(value).split('') + const chars = [...String(value)] for (const [index, input] of this._inputs.entries()) { input.value = chars[index] || '' } @@ -136,7 +136,7 @@ class OtpInput extends BaseComponent { // Handle multi-character input (some browsers/autofill) if (value.length > 1) { // Distribute characters across inputs - const chars = value.split('') + const chars = [...value] input.value = chars[0] || '' for (let i = 1; i < chars.length && index + i < this._inputs.length; i++) { diff --git a/js/src/tab.js b/js/src/tab.js index f02519eb97b8..70375ba25d11 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -162,7 +162,7 @@ class Tab extends BaseComponent { let nextActiveElement if ([HOME_KEY, END_KEY].includes(event.key)) { - nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1] + nextActiveElement = event.key === HOME_KEY ? children[0] : children.at(-1) } else { const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key) nextActiveElement = getNextActiveElement(children, event.target, isNext, true) diff --git a/js/src/tooltip.js b/js/src/tooltip.js index bf5fadb97710..106085906dc7 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -242,7 +242,7 @@ class Tooltip extends BaseComponent { // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { + for (const element of document.body.children) { EventHandler.on(element, 'mouseover', noop) } } @@ -276,7 +276,7 @@ class Tooltip extends BaseComponent { // If this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { + for (const element of document.body.children) { EventHandler.off(element, 'mouseover', noop) } } diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js index 158f3d1846d5..7900ab875a1d 100644 --- a/js/src/util/focustrap.js +++ b/js/src/util/focustrap.js @@ -97,7 +97,7 @@ class FocusTrap extends Config { if (elements.length === 0) { trapElement.focus() } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) { - elements[elements.length - 1].focus() + elements.at(-1).focus() } else { elements[0].focus() } diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index bcd565a9cfef..cb33633ffd39 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -92,7 +92,7 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { const domParser = new window.DOMParser() const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') - const elements = [].concat(...createdDocument.body.querySelectorAll('*')) + const elements = [...createdDocument.body.querySelectorAll('*')] for (const element of elements) { const elementName = element.nodeName.toLowerCase() @@ -102,8 +102,8 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { continue } - const attributeList = [].concat(...element.attributes) - const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []) + const attributeList = [...element.attributes] + const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])] for (const attribute of attributeList) { if (!allowedAttribute(attribute, allowedAttributes)) { diff --git a/js/tests/integration/bundle-modularity.js b/js/tests/integration/bundle-modularity.js index d8f6fc0df844..574f54d219bd 100644 --- a/js/tests/integration/bundle-modularity.js +++ b/js/tests/integration/bundle-modularity.js @@ -2,6 +2,6 @@ import Tooltip from '../../dist/tooltip.js' import '../../dist/carousel.js' // eslint-disable-line import/no-unassigned-import window.addEventListener('load', () => { - [].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]')) + [...document.querySelectorAll('[data-bs-toggle="tooltip"]')] .map(tooltipNode => new Tooltip(tooltipNode)) }) diff --git a/js/tests/integration/bundle.js b/js/tests/integration/bundle.js index 0603bfdfb2dc..9f7404142838 100644 --- a/js/tests/integration/bundle.js +++ b/js/tests/integration/bundle.js @@ -1,6 +1,6 @@ import { Tooltip } from '../../../dist/js/bootstrap.js' window.addEventListener('load', () => { - [].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]')) + [...document.querySelectorAll('[data-bs-toggle="tooltip"]')] .map(tooltipNode => new Tooltip(tooltipNode)) }) diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index f25d9fc3966b..fddea9c01fb1 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -130,7 +130,7 @@ describe('Collapse', () => { const collapseEl1 = fixtureEl.querySelector('#collapse1') const collapseEl2 = fixtureEl.querySelector('#collapse2') - const collapseList = [].concat(...fixtureEl.querySelectorAll('.collapse')) + const collapseList = [...fixtureEl.querySelectorAll('.collapse')] .map(el => new Collapse(el, { parent, toggle: false diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js index 95d9bf8ec9d8..f0ec5faf9a0b 100644 --- a/js/tests/unit/dom/selector-engine.spec.js +++ b/js/tests/unit/dom/selector-engine.spec.js @@ -68,7 +68,7 @@ describe('SelectorEngine', () => { ].join('') const list = fixtureEl.querySelector('ul') - const liList = [].concat(...fixtureEl.querySelectorAll('li')) + const liList = [...fixtureEl.querySelectorAll('li')] const result = SelectorEngine.children(list, 'li') expect(result).toEqual(liList) @@ -356,7 +356,7 @@ describe('SelectorEngine', () => { const testEl = fixtureEl.querySelector('#test') - expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual([...fixtureEl.querySelectorAll('.target')]) }) it('should get elements if several ids are given', () => { @@ -368,7 +368,7 @@ describe('SelectorEngine', () => { const testEl = fixtureEl.querySelector('#test') - expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual([...fixtureEl.querySelectorAll('.target')]) }) it('should get elements if several ids with special chars are given', () => { @@ -380,7 +380,7 @@ describe('SelectorEngine', () => { const testEl = fixtureEl.querySelector('#test') - expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual([...fixtureEl.querySelectorAll('.target')]) }) it('should get elements in array, from href if no data-bs-target set', () => { @@ -392,7 +392,7 @@ describe('SelectorEngine', () => { const testEl = fixtureEl.querySelector('#test') - expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual([...fixtureEl.querySelectorAll('.target')]) }) it('should return empty array if elements not found', () => { diff --git a/site/src/assets/examples/sidebars/sidebars.js b/site/src/assets/examples/sidebars/sidebars.js index 072c38e85b74..6b3177aa433e 100644 --- a/site/src/assets/examples/sidebars/sidebars.js +++ b/site/src/assets/examples/sidebars/sidebars.js @@ -1,6 +1,6 @@ import { Tooltip } from '../../dist/js/bootstrap.bundle.js' -const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]')) +const tooltipTriggerList = [...document.querySelectorAll('[data-bs-toggle="tooltip"]')] tooltipTriggerList.forEach(tooltipTriggerEl => { new Tooltip(tooltipTriggerEl) }) diff --git a/site/src/assets/stackblitz.js b/site/src/assets/stackblitz.js index 1ca38d1949ef..a944ef8a0053 100644 --- a/site/src/assets/stackblitz.js +++ b/site/src/assets/stackblitz.js @@ -31,7 +31,7 @@ export default () => { const htmlSnippet = exampleEl.innerHTML const jsSnippet = codeSnippet.querySelector('.btn-edit').getAttribute('data-sb-js-snippet') // Get extra classes for this example - const classes = Array.from(exampleEl.classList).join(' ') + const classes = [...exampleEl.classList].join(' ') openBootstrapSnippet(htmlSnippet, jsSnippet, classes) }) From 03f9c27cc785d750b1e34244ef86b0f261f3c856 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Wed, 22 Apr 2026 20:18:15 -0700 Subject: [PATCH 2/8] Simplify variants to 2 col --- site/src/content/docs/components/card.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/content/docs/components/card.mdx b/site/src/content/docs/components/card.mdx index cb963aefa9a8..503eeb64356b 100644 --- a/site/src/content/docs/components/card.mdx +++ b/site/src/content/docs/components/card.mdx @@ -375,7 +375,7 @@ And with the reverse layout: Customize cards by using our theme color utilities. By default, cards use `bg` for their background and border colors when applying a theme color. Use `.card-subtle` to swap the background and border colors for a more subtle look. - `
+ `
${themeColor.title}

Card title

From 9cfa06374d31089f8e51aa8774d55adf2a800eac Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Wed, 22 Apr 2026 20:23:56 -0700 Subject: [PATCH 3/8] More card layout improvement --- site/src/content/docs/components/card.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/content/docs/components/card.mdx b/site/src/content/docs/components/card.mdx index 503eeb64356b..8934980731f5 100644 --- a/site/src/content/docs/components/card.mdx +++ b/site/src/content/docs/components/card.mdx @@ -375,7 +375,7 @@ And with the reverse layout: Customize cards by using our theme color utilities. By default, cards use `bg` for their background and border colors when applying a theme color. Use `.card-subtle` to swap the background and border colors for a more subtle look. - `
+ `
${themeColor.title}

Card title

@@ -383,7 +383,7 @@ Customize cards by using our theme color utilities. By default, cards use `bg` f
`)} customMarkup={getData('theme-colors').map((themeColor) => `
`)} /> - `
+ `
${themeColor.title}

Card title

From def084d38605106362d2dcf9a614ba198bbb341a Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Wed, 22 Apr 2026 20:42:54 -0700 Subject: [PATCH 4/8] Improve card docs more --- site/src/content/docs/components/card.mdx | 58 ++++++++++++----------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/site/src/content/docs/components/card.mdx b/site/src/content/docs/components/card.mdx index 8934980731f5..56b921dc136a 100644 --- a/site/src/content/docs/components/card.mdx +++ b/site/src/content/docs/components/card.mdx @@ -8,7 +8,7 @@ css_media: viewport import { getData } from '@libs/data' -Cards are flexible and extensible content containers. They include options for headers and footers, a wide variety of content, contextual background colors, and powerful display options. **Cards have no fixed width to start**, so they’ll naturally fill the full width of its parent element, or slot into your grid columns. This is easily customized with our various [sizing options](#width). +Cards are flexible and extensible content containers with support for headers and footers, a wide variety of content, contextual background colors, and additional display options. **Cards have no fixed width to start**, so they’ll naturally fill the width of their parent element, or slot into your grid columns. @@ -25,9 +25,11 @@ Cards are built with as little markup and styles as possible, but still manage t - Cards are built with flexbox, so they offer easy alignment via [flexbox utilities]([[docsref:/utilities/flex]]) and [grid column classes]([[docsref:/layout/grid#column-classes]]). -- Cards have no `margin` by default, so use [margin utilities]([[docsref:/utilities/margin]]) as needed. +- **Cards require both `.card` and `.card-body`** to best support perfect borders, rounded corners, and various content and layout options. -- Cards are broken down into three categories of sub-components: header, body, and footer. Headers and footers are optional while the body is required. Images can also serve as headers and footers. +- Cards have no `margin` by default, so use [margin utilities]([[docsref:/utilities/margin]]) or [gap utilities]([[docsref:/utilities/gap]]) as needed. + +- Cards are broken down into three categories of sub-components: header, body, and footer. Headers and footers are optional while **the body is required**. Images can also serve as headers and footers. - Card and card body are both `flex` containers, so content can be aligned and stretched as needed with utilities and whatever HTML you require. @@ -37,8 +39,6 @@ Cards are built with as little markup and styles as possible, but still manage t ## Content types -Cards support a wide variety of content, including images, text, list groups, links, and more. Below are examples of what’s supported. - ### Body The building block of a card is the `.card-body`. Use it whenever you need a padded section within a card. @@ -49,6 +49,12 @@ The building block of a card is the `.card-body`. Use it whenever you need a pad
`} /> +For simple cards, you can use the `.card-body` class directly on the card element. + + + This is some text within a card body. +
`} /> + ### Titles, text, and links Titles, text, and links within cards all have required class names for managing alignment. Size and color is up to you to manage. @@ -101,7 +107,7 @@ When combined with other card sub-components, use `.card-list` to manage borders
Featured
-
    +
    • An item
    • A second item
    • A third item
    • @@ -109,7 +115,7 @@ When combined with other card sub-components, use `.card-list` to manage borders
`} /> -
    +
    • An item
    • A second item
    • A third item
    • @@ -182,16 +188,16 @@ Card headers can be styled by adding `.card-header` to `` elements.
`} /> - +
Featured

Special title treatment

With supporting text below as a natural lead-in to additional content.

- Go somewhere + Go somewhere
- `} /> @@ -318,14 +324,14 @@ Similar to headers and footers, cards can include top and bottom “image caps

Card title

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

-

Last updated 3 mins ago

+

Last updated 3 mins ago

Card title

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

-

Last updated 3 mins ago

+

Last updated 3 mins ago

`} /> @@ -356,7 +362,7 @@ Use the `.card-row` class to make a card horizontal. For image caps, use the `.c

Card title

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

-

Last updated 3 mins ago

+

Last updated 3 mins ago

`} /> @@ -366,7 +372,7 @@ And with the reverse layout:

Card title

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

-

Last updated 3 mins ago

+

Last updated 3 mins ago

`} /> @@ -412,7 +418,7 @@ Add `.card-translucent` to blur and saturate the background of a card. Header an

Headers and footers are also made translucent to maintain a consistent look.

Go somewhere - `} /> @@ -431,7 +437,7 @@ Use card groups to render cards as a single, attached element with equal width a

Card title

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

-

Last updated 3 mins ago

+

Last updated 3 mins ago

@@ -439,7 +445,7 @@ Use card groups to render cards as a single, attached element with equal width a

Card title

This card has supporting text below as a natural lead-in to additional content.

-

Last updated 3 mins ago

+

Last updated 3 mins ago

@@ -447,7 +453,7 @@ Use card groups to render cards as a single, attached element with equal width a

Card title

This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.

-

Last updated 3 mins ago

+

Last updated 3 mins ago

`} /> @@ -462,7 +468,7 @@ When using card groups with footers, their content will automatically line up.

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

@@ -472,7 +478,7 @@ When using card groups with footers, their content will automatically line up.

This card has supporting text below as a natural lead-in to additional content.

@@ -482,7 +488,7 @@ When using card groups with footers, their content will automatically line up.

This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.

`} /> @@ -623,7 +629,7 @@ Just like with card groups, card footers will automatically line up.

This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

@@ -635,7 +641,7 @@ Just like with card groups, card footers will automatically line up.

This card has supporting text below as a natural lead-in to additional content.

@@ -647,16 +653,12 @@ Just like with card groups, card footers will automatically line up.

This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.

`} /> -### Masonry - -In `v4` we used a CSS-only technique to mimic the behavior of [Masonry](https://masonry.desandro.com/)-like columns, but this technique came with lots of unpleasant [side effects](https://github.com/twbs/bootstrap/pull/28922). If you want to have this type of layout in `v5`, you can just make use of Masonry plugin. **Masonry is not included in Bootstrap**, but we’ve made a [demo example]([[docsref:/examples/masonry]]) to help you get started. - ## CSS ### Variables From 7c570b36da77ebb24b158f9cb1e482c7404574ad Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Wed, 22 Apr 2026 20:43:09 -0700 Subject: [PATCH 5/8] text-body-secondary is now fg-2 --- js/tests/visual/datepicker.html | 4 +- js/tests/visual/menu-submenu.html | 20 ++-- js/tests/visual/toast.html | 2 +- js/tests/visual/tooltip.html | 2 +- site/src/assets/examples/album/index.astro | 24 ++--- site/src/assets/examples/blog/index.astro | 12 +-- site/src/assets/examples/carousel/index.astro | 6 +- .../assets/examples/cheatsheet/index.astro | 8 +- site/src/assets/examples/checkout/index.astro | 20 ++-- .../src/assets/examples/dashboard/index.astro | 2 +- site/src/assets/examples/dialogs/index.astro | 12 +-- .../assets/examples/drawer-navbar/index.astro | 12 +-- site/src/assets/examples/features/index.astro | 26 ++--- site/src/assets/examples/footers/index.astro | 94 +++++++++---------- site/src/assets/examples/heroes/index.astro | 2 +- .../src/assets/examples/jumbotron/index.astro | 2 +- .../assets/examples/list-groups/index.astro | 20 ++-- site/src/assets/examples/masonry/index.astro | 10 +- site/src/assets/examples/menus/index.astro | 4 +- site/src/assets/examples/pricing/index.astro | 10 +- site/src/assets/examples/product/index.astro | 2 +- site/src/assets/examples/sidebars/index.astro | 22 ++--- site/src/assets/examples/sign-in/index.astro | 2 +- .../examples/starter-template/index.astro | 2 +- .../examples/sticky-footer-navbar/index.astro | 2 +- .../assets/examples/sticky-footer/index.astro | 2 +- site/src/components/footer/Footer.astro | 2 +- .../components/home/ComponentUtilities.astro | 2 +- site/src/components/home/MastHead.astro | 2 +- site/src/components/home/Plugins.astro | 2 +- site/src/content/docs/about/brand.mdx | 2 +- .../content/docs/components/list-group.mdx | 8 +- site/src/content/docs/components/menu.mdx | 2 +- site/src/content/docs/components/navbar.mdx | 2 +- site/src/content/docs/components/toasts.mdx | 12 +-- site/src/content/docs/layout/containers.mdx | 14 +-- site/src/layouts/partials/ExamplesMain.astro | 4 +- 37 files changed, 188 insertions(+), 188 deletions(-) diff --git a/js/tests/visual/datepicker.html b/js/tests/visual/datepicker.html index 0bf12ec7f072..e9e719edb81e 100644 --- a/js/tests/visual/datepicker.html +++ b/js/tests/visual/datepicker.html @@ -8,7 +8,7 @@
-

Datepicker Bootstrap Visual Test

+

Datepicker Bootstrap Visual Test


@@ -94,7 +94,7 @@

JavaScript Initialization

-
+
diff --git a/js/tests/visual/menu-submenu.html b/js/tests/visual/menu-submenu.html index 2eb326cfc209..eb07538826ae 100644 --- a/js/tests/visual/menu-submenu.html +++ b/js/tests/visual/menu-submenu.html @@ -44,7 +44,7 @@
-

Menu Submenus Bootstrap Visual Test

+

Menu Submenus Bootstrap Visual Test

Keyboard Navigation: @@ -59,7 +59,7 @@

Menu Submenus Bootstrap Visu

Basic Submenu

-

Single level submenu with hover and click activation.

+

Single level submenu with hover and click activation.

@@ -89,7 +89,7 @@

Basic Submenu

Nested Submenus (Multi-level)

-

Three levels of nested submenus.

+

Three levels of nested submenus.

@@ -126,7 +126,7 @@

Nested Submenus (Multi-level)

Multiple Submenus at Same Level

-

Multiple submenu triggers in the same menu - opening one closes the other.

+

Multiple submenu triggers in the same menu - opening one closes the other.

@@ -175,7 +175,7 @@

Multiple Submenus at Same Level

Viewport Detection (Flipping)

-

Submenus flip to the opposite side when there's not enough space. Try the one on the right.

+

Submenus flip to the opposite side when there's not enough space. Try the one on the right.

@@ -220,7 +220,7 @@

Viewport Detection (Flipping)

Navbar Integration

-

Submenus work within navbar menus.

+

Submenus work within navbar menus.

@@ -127,7 +127,7 @@ import Placeholder from "@shortcodes/Placeholder.astro"
- 9 mins + 9 mins
@@ -142,7 +142,7 @@ import Placeholder from "@shortcodes/Placeholder.astro"
- 9 mins + 9 mins

@@ -158,7 +158,7 @@ import Placeholder from "@shortcodes/Placeholder.astro" - 9 mins + 9 mins @@ -173,7 +173,7 @@ import Placeholder from "@shortcodes/Placeholder.astro" - 9 mins + 9 mins @@ -188,7 +188,7 @@ import Placeholder from "@shortcodes/Placeholder.astro" - 9 mins + 9 mins @@ -199,7 +199,7 @@ import Placeholder from "@shortcodes/Placeholder.astro" -