--- name: bootstrap-v5-v6-migration description: Migrate projects from Bootstrap 5 to Bootstrap 6. Use when upgrading from Bootstrap 5, migrating v5 to v6, or updating v5 class names, components, Sass, or JavaScript to the latest version. For Bootstrap 4 projects, start with the v4-to-v6 migration skill instead. guide: /guides/migration --- # Bootstrap v5 to v6 Migration ## Workflow Work through each step in order. After each step, search the codebase for remaining v5 patterns before moving on. - [ ] Step 1: Update dependencies and build setup - [ ] Step 2: Rename CSS classes and data attributes - [ ] Step 3: Restructure component HTML - [ ] Step 4: Update JavaScript - [ ] Step 5: Update Sass - [ ] Step 6: Verify --- ## Step 1: Dependencies & Build 1. Update `package.json`: `"bootstrap": "6.0.0-alpha.1"` 2. Replace `@popperjs/core` with `@floating-ui/dom` 3. If using Datepicker, add peer dep `vanilla-calendar-pro` 4. Sass: replace all `@import` with `@use` (Node Sass is no longer supported) `bootstrap.bundle.js` includes Floating UI and Vanilla Calendar Pro. Standalone `bootstrap.js` leaves both peer dependencies external. Direct browser loading of the standalone build needs an import map. ```scss // v5 @import "bootstrap/scss/bootstrap"; // v6 @use "bootstrap/scss/bootstrap"; // v6 with overrides — customize CSS tokens through the token maps @use "bootstrap/scss/bootstrap" with ( $root-tokens: ( --spacer: 1rem, ) ); ``` Customize by overriding the CSS token maps — `$root-tokens` for global tokens and each component's `$*-tokens` map — rather than legacy Sass scalars. See [Customize › Sass](https://getbootstrap.com/docs/6.0/customize/sass/#compile-time-overrides). Token names are written **unprefixed** in Sass (e.g. `--spacer`, `--border-radius`); Bootstrap's dist/CDN CSS runs PostCSS to add the `--bs-` prefix (`--bs-spacer`), so when you compile the source yourself the properties stay unprefixed. --- ## Step 2: CSS Class & Attribute Renames ### Responsive & state prefix syntax v6 moves breakpoints and pseudo-states from infix/suffix to prefix with colon. Also renames `xxl` to `2xl`. **Pattern:** `.{class}-{bp}-{value}` becomes `.{bp}:{class}-{value}` and `.{class}-{bp}` becomes `.{bp}:{class}` | v5 | v6 | | --- | --- | | `.d-md-none`, `.p-lg-3` | `.md:d-none`, `.lg:p-3` | | `.col-md-6` | `.md:col-6` | | `.row-cols-md-3` | `.md:row-cols-3` | | `.offset-md-2` | `.md:offset-2` | | `.g-md-3`, `.gx-md-3` | `.md:g-3`, `.md:gx-3` | | `.g-col-md-4` | `.md:g-col-4` | | `.container-sm` | `.sm:container` | | `.navbar-expand-md` | `.md:navbar-expand` | | `.offcanvas-md` | `.md:drawer` | | `.table-responsive-md` | `.md:table-responsive` | | `.list-group-horizontal-md` | `.md:list-group-horizontal` | | `.sticky-md-top` | `.md:sticky-top` | | `.vstack-md` | `.md:vstack` | | `.dialog-fullscreen-sm-down` | `.sm-down:dialog-fullscreen` | | `.d-print-none` | `.print:d-none` | | `.opacity-50-hover` | `.hover:opacity-50` | The prefix does not identify the query type. Utilities, grid columns, containers, drawers, and responsive tables use viewport queries. Stacks, CSS Grid columns, navbar expansion, horizontal list groups, card groups, and stacked tables use container queries. Add the query container that each component’s documentation requires. ### Component renames Three components have been fully renamed. Find-and-replace these prefixes across classes, data attributes, events, JS imports, and CSS variables. #### Modal -> Dialog | Scope | v5 | v6 | | --- | --- | --- | | Classes | `.modal`, `.modal-header/body/footer/title` | `.dialog`, `.dialog-header/body/footer/title` | | Sizes | `.modal-sm/lg/xl/fullscreen` | `.dialog-sm/lg/xl/fullscreen` | | Data attrs | `data-bs-toggle="modal"`, `data-bs-dismiss="modal"` | `data-bs-toggle="dialog"`, `data-bs-dismiss="dialog"` | | JS export | `Modal` | `Dialog` | | Events | `*.bs.modal` | `*.bs.dialog` | | CSS vars | `--modal-*` | `--dialog-*` | | Body class | `.modal-open` on `` | `.dialog-open` on `` | Remove `.modal-dialog` and `.modal-content` wrappers entirely — see Step 3. #### Offcanvas -> Drawer | Scope | v5 | v6 | | --- | --- | --- | | Classes | `.offcanvas`, `.offcanvas-start/end/top/bottom/header/body/title` | `.drawer`, `.drawer-start/end/top/bottom/header/body/title` | | Data attrs | `data-bs-toggle="offcanvas"`, `data-bs-dismiss="offcanvas"` | `data-bs-toggle="drawer"`, `data-bs-dismiss="drawer"` | | JS export | `Offcanvas` | `Drawer` | | Events | `*.bs.offcanvas` | `*.bs.drawer` | | CSS vars | `--offcanvas-*` | `--drawer-*` | | Sass | `$zindex-offcanvas` | `$zindex-drawer` | #### Dropdown -> Menu | Scope | v5 | v6 | | --- | --- | --- | | Classes | `.dropdown-menu`, `.dropdown-item`, `.dropdown-divider`, `.dropdown-header` | `.menu`, `.menu-item`, `.menu-divider`, `.menu-header` | | Data attrs | `data-bs-toggle="dropdown"` | `data-bs-toggle="menu"` | | JS export | `Dropdown` | `Menu` | | Events | `*.bs.dropdown` | `*.bs.menu` | | Sass | `$zindex-dropdown` | `$zindex-menu` | Also remove: `.dropdown-toggle` (no longer needed), `.dropdown` wrapper, `.dropdown-toggle-split`. See Step 3 for new markup. ### Button & badge variants -> theme tokens Per-color classes are replaced by variant + `.theme-*` composition. Built-in theme keys are `primary`, `accent`, `success`, `danger`, `warning`, `info`, `inverse`, and `secondary`. Apply both classes to the same element. | v5 | v6 | | --- | --- | | `.btn-primary` | `class="btn-solid theme-primary"` | | `.btn-outline-primary` | `class="btn-outline theme-primary"` | | `.alert-primary` | `class="alert theme-primary"` | | `.badge.bg-primary` | `class="badge theme-primary"` | New button variants: `.btn-solid`, `.btn-outline`, `.btn-subtle`, `.btn-text`, `.btn-styled`, `.btn-link`. Use `.theme-reset` on a nested subtree to drop inherited `--theme-*` tokens and return to component defaults. There are no `.theme-light` or `.theme-dark` classes. Review v5 light and dark variants manually. Use neutral surface utilities, color modes, or `.theme-inverse` as appropriate. ### Placeholder animations Use `.placeholder-wave` for the standard loading animation. Replace `.placeholder-glow` with `.placeholder-pulse` when you want the old opacity animation. `.placeholder-glow` remains as a compatibility alias. ### Utility class renames | v5 | v6 | | --- | --- | | `.text-primary`, `.text-danger`, etc. | `.fg-primary`, `.fg-danger`, etc. | | `.text-muted` | `.fg-secondary` | | `.mh-*` | `.max-h-*` | | `.mw-*` | `.max-w-*` | | `.form-select` | `.form-control` (on ``. No wrapper and no inline SVG — the mark is a CSS `mask` on `::before`. Radios (`.radio`) follow the same pattern. Switches keep a `.switch` wrapper around the input. ```html
``` Radio and switch equivalents: ```html
``` ### Toggle buttons Input is now nested inside the label. `.btn-check` goes on the label. No `id`/`for` needed. ```html ``` ### Range `.form-range` is now a wrapper and a JavaScript hook. Move the class from the input to a wrapper and add `.form-range-input` to the input. ```html
``` ### Breadcrumbs Add `.breadcrumb-link` on `` elements. Add empty `.breadcrumb-divider` separators between items (replaces `::before` pseudo-elements). An empty divider renders a default chevron via a CSS mask. The default bottom margin is gone (v5 used `$spacer`). ```html ``` --- ## Step 4: JavaScript ### ESM-only All dist files are now ES modules. No more UMD bundles or `window.bootstrap` global. ```html ``` Replace global namespace access with imports: ```js // v5 — global namespace const tooltip = bootstrap.Tooltip.getOrCreateInstance(el) ``` ```js // v6 — explicit import import { Tooltip } from './bootstrap.bundle.min.js' const tooltip = Tooltip.getOrCreateInstance(el) ``` Data API initialization still runs automatically after you add `type="module"`. Apply the component and data attribute renames in this skill. Bundler imports (`import { X } from 'bootstrap'`) work as before. The bundled build includes Floating UI and Vanilla Calendar Pro. The standalone build requires installed peers or a browser import map. ### Renamed JS exports | v5 | v6 | | --- | --- | | `Modal` | `Dialog` | | `Offcanvas` | `Drawer` | | `Dropdown` | `Menu` | ### Popper -> Floating UI Replace `@popperjs/core` with `@floating-ui/dom`. Rename the `popperConfig` option to `floatingConfig` on Tooltip, Popover, and Menu. ### Lifecycle methods return a promise `show()`, `hide()`, `toggle()`, and `close()` now return a promise that resolves when the transition ends. You can `await` them instead of listening for `shown.bs.*` / `hidden.bs.*`. The events still fire. Hover and focus tooltips stay open while the pointer or focus is on the tip itself (WCAG 1.4.13). Press Escape or leave both the trigger and the tip to hide it. Popup datepickers close on outside interaction, on Escape, or when focus leaves the input and calendar. ### TypeScript declarations Bootstrap’s source is now TypeScript. The package includes type declarations, so remove `@types/bootstrap`. Deep `.js` imports resolve to compiled `js/dist` files. ### Removed - jQuery support - `bootstrap.esm.js` / `bootstrap.esm.min.js` — use `bootstrap.js` - `js/index.umd.js` ### Validation JS ```js // v5 document.querySelectorAll('.needs-validation') form.classList.add('was-validated') // v6 document.querySelectorAll('form[data-bs-validate]') // Remove the was-validated line entirely ``` --- ## Step 5: Sass ### Renamed files | v5 | v6 | | --- | --- | | `_variables.scss` | `_config.scss` | | `_variables-dark.scss` | Removed (merged into `_theme.scss`) | | `_maps.scss` | Removed | | `_placeholders.scss` | `_placeholder.scss` | | `_spinners.scss` | `_spinner.scss` | | `_form-check.scss` | `_check.scss`, `_radio.scss`, `_switch.scss` | | `mixins/_forms.scss` | `mixins/_form-validation.scss` | | `forms/_form-variables.scss` | Removed | | `vendor/_rfs.scss` | Removed | ### Renamed variables and functions | v5 | v6 | | --- | --- | | `$grid-breakpoints` | `$breakpoints` | | `$border-radius`, `$border-radius-sm/lg/xl/xxl`, `$border-radius-pill` | Removed — see the radius scale below | | `$text-muted` | Use secondary color | | `$hr-bg-color` | `$hr-border-color` | | `$hr-height` | `$hr-border-width` | | `$zindex-dropdown` | `$zindex-menu` | | `$zindex-offcanvas` | `$zindex-drawer` | | `$form-validation-states` | `$validation-states` | | `$btn-close-white-filter` | Removed — icon uses `currentcolor` | | `$enable-negative-margins` | Removed — negative margins are always generated; customize `$negative-spacers` | | `add()` / `subtract()` | `calc()` | | `breakpoint-infix()` | `breakpoint-prefix()` (returns `"md\:"` not `"-md"`) | | `$infix` (in loop mixins) | `$prefix` | | `$prefix` (CSS var prefix) | Removed — use PostCSS instead | ### Border radius scale The `$border-radius-*` variables are gone. v6 uses a single base `$radius: .5rem` and a `$radii` map (keys `0`–`9`, e.g. `5: $radius`, `9: $radius * 2`), exposed as `--radius-0`–`--radius-9` tokens (plus `--radius-pill`). The `.rounded-*` utilities now span `0`–`9` and map to different values than v5, so shift class numbers up to keep the same roundness (e.g. `.rounded-1` → `.rounded-3`, `.rounded-3` → `.rounded-5`). Override the base or the map entries rather than the old per-size variables: ```scss // $radius lives in _config.scss, so configure that module before loading Bootstrap @use "bootstrap/scss/config" with ( $radius: .375rem // scales the whole map ); @use "bootstrap/scss/bootstrap"; ``` ### Removed (no replacement) - `$nested-kbd-font-weight` - `$enable-dark-mode` — dark mode is always compiled - `$enable-validation-icons` - `$enable-caret`, `$caret-width`, `$caret-vertical-align`, `$caret-spacing` - `$accordion-button-focus-border-color`, `$tooltip-arrow-color` - `$popover-arrow-color`, `$popover-arrow-outer-color` - `$alert-bg-scale`, `$alert-border-scale`, `$alert-color-scale` - `$list-group-item-bg-scale`, `$list-group-item-color-scale` - `$carousel-dark-indicator-active-bg`, `$carousel-dark-control-icon-filter` - `$dropdown-header-padding` - All `*-focus-box-shadow` variables — use `focus-ring()` mixin with `--focus-ring-*` CSS custom properties - RFS mixins — use `clamp()` for responsive sizing - `create-css-vars()` mixin - Caret mixins (`caret()`, `caret-down()`, `caret-up()`, `caret-end()`, `caret-start()`) — add an icon to the toggle markup instead - `muted`, `black-50`, `white-50` from text color utilities map ### Utility API Removed `css-var`, `css-variable-name`, and `local-vars` options. Use `property` map and `variables` instead. ### Theme maps merge `$theme-colors`, `$theme-bgs`, `$theme-fgs`, `$theme-borders`, `$badge-variants`, and other global maps use `defaults()`. Pass only the keys you want to change through `@use ... with ()`. Set a key to `null` to drop it. Nested theme-color maps merge one level deep — pass the whole sub-map to change one role. Root tokens emit on `:root, :host`. Repeat `:root` overrides on `:host` if you use shadow DOM. --- ## Rebuilt behavior & new components Beyond the renames above, several things changed how a v5 project behaves or what's available. Check the docs for full markup/options. ### Carousel — rebuilt on CSS scroll-snap The markup (`.carousel` → `.carousel-inner` → `.carousel-item`) and the JS API (`next`/`prev`/`to`/`cycle`/`pause`, `slide`/`slid` events) are preserved, but several things changed: - **`ride` → `autoplay` (boolean, opt-in).** `data-bs-ride="carousel"` becomes `data-bs-autoplay="true"`; `{ ride: 'carousel' }` becomes `{ autoplay: true }`. Default is `autoplay: false`, so a v5 carousel that auto-advanced now sits still until you opt in. Interacting with an autoplaying carousel now **permanently stops** it (WCAG 2.2.2). - **`wrap` → `ends`.** `wrap: true` → `ends: "wrap"` (or the new default `"loop"`); `wrap: false` → `ends: "stop"`. Set via `data-bs-ends`. The `touch` option is removed (native scroll handles it). - **Removed classes:** `.carousel-control-prev/next` (compose a `.btn-icon` + `data-bs-slide` + `.carousel-icon-prev/next`), `.carousel-caption` (use your own markup), `.carousel-dark` (use `data-bs-theme="dark"`), `.carousel-stacked` (now the default), and the transitional `.carousel-item-start/end/next/prev`. Overlaid controls now require `.carousel-overlay`. Control-icon classes renamed `.carousel-control-prev-icon` → `.carousel-icon-prev` (and `-next`). ### ScrollSpy — rebuilt on IntersectionObserver The `data-bs-spy="scroll"` markup and the `activate.bs.scrollspy` event are unchanged, but detection is now driven by an **activation line** (no scroll-position polling), and the options changed: - **Removed the deprecated `offset` and `method` options** (deprecated since v5.1.3) — no longer parsed. - **Added `topMargin`** (default `12%`) — positions the activation line as a `%` or `px` value from the top of the scroll root (e.g. `96px` to sit below a sticky navbar). This is the everyday knob; use it instead of `rootMargin`. - **`rootMargin`** is now an advanced override (default `null`, was `'0px 0px -25%'`); when set it takes precedence over `topMargin` and is passed straight to the observer. - **`threshold`** default changed from `[0.1, 0.5, 1]` to `[0]`. - With **`smoothScroll`**, clicking a link now restores the URL hash (`history.replaceState`) and moves focus to the target once the scroll settles (better keyboard/AT nav). - Target `id`s are resolved with `getElementById`, so ids with dots, colons, slashes, or percent-encoding work without manual escaping. ### Collapse — simplified state and transitions - Remove selectors and tests that depend on `.collapsing`. Collapse now toggles `.show` and uses native size interpolation. - Replace trigger `.collapsed` selectors with `[aria-expanded="false"]`. - Remove the `toggle` constructor option. Call `show()` or `hide()` when initialization must change state. ### New components (didn't exist in v5) | Component | Trigger / hook | Purpose | | --- | --- | --- | | Combobox | `data-bs-toggle="combobox"` | Filterable/autocomplete select built on Menu | | Chip / Chip input | `.chip`, `.chip-input` (`data-bs-chips`) | Tags / tokens + interactive entry | | Datepicker | `data-bs-toggle="datepicker"` | Date picker (peer dep `vanilla-calendar-pro`) | | Range | `.form-range` (+ `data-bs-bubble`, ticks) | Enhanced range slider with a value bubble | | Strength | `data-bs-strength` | Password-strength meter | | OTP input | `data-bs-otp` | One-time-code input | | Nav overflow | `.nav-overflow` wrapper around a `.nav` | Collapses overflowing nav items into a menu | | Toggler | `data-bs-toggle="toggler"` | Generic class/attribute toggler | | Submenu | `.submenu` (within Menu) | Nested menus (`submenuTrigger`, `submenuDelay`) | | Stepper | `.stepper` | Multi-step workflow (CSS-only) | | Avatar | `.avatar` | Avatars with sizes, status, `.avatar-stack` (CSS-only) | | Form adorn | `.form-adorn` | Icon/text decoration on inputs (CSS-only) | | Prose | `.prose` / `.not-prose` | Rich-typography scoping (CSS-only) | ### Removed / changed internals - **`util/backdrop.js`, `util/focustrap.js`, and `util/scrollbar.js` removed.** Dialog and Drawer use the native `` element, which provides the backdrop (`::backdrop`), the focus trap, and an inert top layer; the body scroll-lock is now CSS (`:root.dialog-open`). If you imported `bootstrap/js/src/util/backdrop`, `.../focustrap`, or `.../scrollbar` directly, they're gone. - **CSS `@layer`.** Component styles are wrapped in cascade layers (`colors, theme, config, root, reboot, layout, content, forms, components, custom, helpers, utilities`). Author CSS outside any layer now wins over Bootstrap regardless of source order — if your v5 overrides relied on specificity or load order, re-check them. - **`--bs-*-rgb` variables removed.** The `$*-rgb` Sass vars and `--bs-*-rgb` custom properties are gone. Replace `rgba(var(--bs-primary-rgb), .5)` with `color-mix(in oklab, var(--bs-primary), transparent 50%)` (or use the color directly). --- ## Step 6: Verify 1. Build the project and fix any compilation errors. 2. Search for remaining v5 patterns: - `modal` classes/attributes (should be `dialog`) - `offcanvas` (should be `drawer`) - `dropdown` (should be `menu`) - `d-md-`, `d-lg-`, `col-md-`, etc. (should use `md:` prefix syntax) - `btn-primary`, `btn-outline-` (should use `.btn-solid .theme-*`) - `.text-primary`, `.text-danger` (should be `.fg-*`) - `@import` in Sass (should be `@use`) - `form-select` (should be `form-control`) - `was-validated`, `needs-validation` (should be `data-bs-validate`) - `popperConfig` (should be `floatingConfig`) - `form-check` (should be `check`, `radio`, or `switch`) - `var(--bs-*-rgb)` / `rgba(var(--bs-…-rgb)` (removed — use `color-mix()`) - `$border-radius` Sass vars (removed — use `$radius` / `$radii` / `--radius-*`) - child `` inside `.btn-close` (should be empty — icon is a CSS mask) - `.btn-close-white` (removed — set text `color` instead) - `
` (must be a native ``) - `data-bs-ride` (renamed to `data-bs-autoplay`); `wrap:` carousel option (now `ends:`) - `.carousel-control-prev/next`, `.carousel-caption`, `.carousel-dark`, `.carousel-stacked` (all removed) - `.fs-1`–`.fs-6` (should be `.fs-4xl` … `.fs-md`) - `.link-offset-*` / `.link-underline-*` (now `.underline-offset-*` / `.underline-*`) - ScrollSpy `offset` / `method` options (removed — use `topMargin`) - inline SVG inside `.check` (icon is a CSS mask on the input) - `$theme-colors` full-map replacements (v6 merges via `defaults()`) - `.collapsing`, `.collapsed` trigger selectors, and the Collapse `toggle` option - responsive stacks, CSS Grid, navbars, list groups, card groups, and stacked tables without a query container 3. Test in browser — v6 requires support for `oklch()` and `color-mix()`.