Bootstrap v4 to v6 Migration
There is no direct v4 to v6 path. Migrate in two stages — v4 to v5 first (this skill), then v5 to v6 (the bootstrap-v5-v6-migration skill). Skipping the middle stage doesn't work, because many v4 names were renamed in v5 and renamed again in v6: .text-primary becomes .fg-primary only after passing through v5, and .badge-primary becomes .badge.theme-primary by way of .badge.bg-primary.
Workflow
Work through each step in order. After each step, search the codebase for remaining v4 patterns before moving on.
- Step 1: Plan the two-stage migration
- Step 2: v4 to v5 — dependencies, build, and Sass
- Step 3: v4 to v5 — JavaScript
- Step 4: v4 to v5 — classes and attributes
- Step 5: v5 to v6 — hand off to the v6 migration skill
- Step 6: Verify
Step 1: Plan the two-stage migration
- Get the project onto Bootstrap 5 and working — build passing, pages rendering, tests green — before touching v6. Landing the v5 stage as its own commit (or PR) keeps the two rename waves separable when something breaks.
- Inventory what you're dealing with first. The v4 features that cost the most time are custom forms,
.input-group-append/.input-group-prepend, jumbotrons,.mediaobjects,.card-deck/.card-columns, and any jQuery that calls Bootstrap plugins. - Decide about jQuery separately. Bootstrap 5 dropped it, but your app may use it for other things; removing Bootstrap's dependency on it does not require removing jQuery itself.
- Note the browser floor. Bootstrap 5 drops IE. Bootstrap 6 requires Chrome and Edge 130, Firefox 132, and Safari 18 on macOS and iOS.
Step 2: v4 to v5 — dependencies, build, and Sass
-
Update the dependency and drop the jQuery/Popper v1 pair:
npm i --save bootstrap@5 @popperjs/core npm uninstall popper.jspopper.js(v1) is replaced by@popperjs/core(v2). Bootstrap 5's bundle includes Popper; the standalonebootstrap.jsdoes not. -
Switch the Sass compiler to Dart Sass (
npm i --save-dev sass). Node Sass / LibSass is not supported. Keep using@importfor now — v5 still uses it; the move to@usehappens in the v6 stage. -
Sass maps no longer merge automatically. In v4, adding one key to
$theme-colorswas enough; in v5 you must define the complete map, including the keys you're keeping. -
Update renamed functions and mixins:
v4 v5 color-yiq()color-contrast()theme-color(),gray()Removed — reference $theme-colors/$graysdirectlytheme-color-level()tint-color()/shade-color()lighten()/darken()tint-color()/shade-color()media-breakpoint-down(md)media-breakpoint-down(lg)— takes the breakpoint itself, not the next one downmedia-breakpoint-between(sm, md)media-breakpoint-between(sm, lg)— same shift on the upper boundThe breakpoint parameter change is silent: nothing errors, your media queries just apply one breakpoint off. Audit every call.
-
Print styles,
$enable-*flags, and the utility API all changed shape in v5. If you generated custom utilities with v4 mixins, rewrite them through v5's$utilitiesmap.
Step 3: v4 to v5 — JavaScript
-
Namespace every data attribute with
bs. This is a mechanical but wide-reaching change:v4 v5 data-toggledata-bs-toggledata-targetdata-bs-targetdata-dismissdata-bs-dismissdata-parentdata-bs-parentdata-ridedata-bs-ridedata-slide,data-slide-todata-bs-slide,data-bs-slide-todata-spydata-bs-spydata-backdrop,data-keyboarddata-bs-backdrop,data-bs-keyboarddata-content,data-placement,data-triggerdata-bs-content,data-bs-placement,data-bs-triggerAny option passed as an attribute follows the same rule:
data-<option>becomesdata-bs-<option>. -
Replace jQuery plugin calls with constructors or static getters:
// v4 $('#myModal').modal('show') $('#myTooltip').tooltip() // v5 const modal = new bootstrap.Modal(document.querySelector('#myModal')) modal.show() bootstrap.Tooltip.getOrCreateInstance(document.querySelector('#myTooltip')) -
Replace jQuery event handlers with native listeners. Bootstrap 5 dispatches native
CustomEvents, so$(el).on('shown.bs.modal', …)stops firing:// v5 document.querySelector('#myModal').addEventListener('shown.bs.modal', event => { // event.relatedTarget replaces event.relatedTarget from the jQuery event }) -
Smaller API renames:
_getInstance()→getInstance(), and prefer the newgetOrCreateInstance().- The sanitizer option
whiteList→allowList. - Tooltip and Popover
title/contentstill accept functions, butthisis no longer the jQuery element.
-
If you relied on jQuery's
noConflictor on$.fn.button('toggle')-style calls, rewrite them against the class API — there is no jQuery interface left to shim.
Step 4: v4 to v5 — classes and attributes
Directional utilities become logical (RTL support)
| v4 | v5 |
|---|---|
.ml-*, .mr-* |
.ms-*, .me-* |
.pl-*, .pr-* |
.ps-*, .pe-* |
.float-left, .float-right |
.float-start, .float-end |
.text-left, .text-right |
.text-start, .text-end |
.border-left, .border-right |
.border-start, .border-end |
.rounded-left, .rounded-right |
.rounded-start, .rounded-end |
.dropleft, .dropright |
.dropstart, .dropend |
The responsive variants follow the same rename (.ml-md-3 → .ms-md-3).
Typography and helpers
| v4 | v5 |
|---|---|
.font-weight-bold, -normal, -light, -bolder, -lighter |
.fw-bold, .fw-normal, .fw-light, .fw-bolder, .fw-lighter |
.font-italic |
.fst-italic |
.text-monospace |
.font-monospace |
.sr-only, .sr-only-focusable |
.visually-hidden, .visually-hidden-focusable |
.embed-responsive, .embed-responsive-16by9 |
.ratio, .ratio-16x9 |
.text-justify, .text-hide, .pre-scrollable |
Removed |
.rounded-sm, .rounded-lg |
.rounded-1, .rounded-3 |
Forms
This is the largest v4 to v5 rewrite: custom form controls were merged into the standard classes.
| v4 | v5 |
|---|---|
.custom-select |
.form-select |
.custom-range |
.form-range |
.custom-file, .form-control-file |
.form-control (on <input type="file">) |
.form-control-range |
.form-range |
.custom-control.custom-checkbox |
.form-check |
.custom-control.custom-radio |
.form-check |
.custom-control.custom-switch |
.form-check.form-switch |
.custom-control-input |
.form-check-input |
.custom-control-label |
.form-check-label |
.custom-control-inline |
.form-check-inline |
.form-group |
Spacing utilities, e.g. .mb-3 |
.form-row |
.row plus gutter utilities (.g-*) |
.form-inline |
Grid or flex utilities (.row .row-cols-lg-auto, .d-flex) |
<label> with no class |
<label class="form-label"> |
Input groups lose their wrappers: delete .input-group-append and .input-group-prepend and place .input-group-text, buttons, and controls as direct children of .input-group. Validation feedback moves out of the removed wrappers too.
Components dropped or restructured
| v4 | v5 |
|---|---|
.jumbotron |
Removed — compose with .bg-*, .p-*, .rounded-* |
.media |
Removed — use .d-flex and gap/margin utilities |
.card-deck, .card-columns |
Removed — use the grid, or CSS columns for masonry |
.close |
.btn-close (drop the × child; the icon is a background image) |
.badge-primary and friends |
.badge plus .bg-primary |
.badge-pill |
.rounded-pill |
.btn-block |
A .d-grid wrapper (add .gap-* for stacked buttons) |
.thead-light, .thead-dark |
.table-light, .table-dark on <thead> |
.arrow (tooltip/popover) |
.tooltip-arrow, .popover-arrow |
.carousel-item-left, -right |
.carousel-item-start, -end |
Badges also lost their link styling — a <a class="badge"> no longer gets hover/focus states.
Grid, navbars, and navs
.no-gutters→.g-0, and gutters are now controlled by the.g-*,.gx-*,.gy-*utilities.- A new
xxlbreakpoint (1400px) exists; existingxlclasses keep their meaning. - Navbars now require a container inside
.navbar(<nav class="navbar"><div class="container">…). - For tabs and pills,
.activemoves from.nav-itemto.nav-link..nav-itemis optional as a wrapper. .dropdown-menuitems can be<button>or<a>;.dropdown-item-textwas added for plain text.
Step 5: v5 to v6 — hand off to the v6 migration skill
Once the project builds and renders on Bootstrap 5, switch to the bootstrap-v5-v6-migration skill and work through its steps. Do not attempt those renames while v4 patterns are still present — its find-and-replace steps assume v5 names.
That stage covers, among other things:
@importto@use, and the move from Sass scalars to CSS token maps (defaults()merges theme maps again in v6)- Popper to Floating UI, plus Vanilla Calendar Pro for Datepicker. The bundled build includes both peers; the standalone build leaves them external.
- The
md:responsive prefix syntax replacing-md-infixes - Modal to Dialog, Offcanvas to Drawer, Dropdown to Menu. Dialog and Drawer use native
<dialog>elements. .btn-primaryto.btn-solid .theme-primary, and.text-*to.fg-*- ESM-only JavaScript with no
window.bootstrapglobal - Container-query contexts for responsive stacks, CSS Grid, navbars, list groups, card groups, and stacked tables
Two v4-era details worth flagging before you start it: the .close → .btn-close element you produced in Step 4 keeps that name in v6 (but must stay empty, with no child SVG), and the .form-check markup you just wrote gets rebuilt again into v6's .check / .radio / .switch structure.
Step 6: Verify
- Build the project and fix compilation errors before looking at the rendered output.
- Search for leftover v4 patterns:
data-toggle=,data-target=,data-dismiss=,data-ride=,data-slide,data-spy=(should all bedata-bs-*)class="close",×inside a close button (should be.btn-close)badge-,badge-pill(should be.badge .bg-*/.rounded-pill)custom-select,custom-control,custom-switch,custom-range,custom-fileform-group,form-row,form-inline,input-group-append,input-group-prepend\bm[lr]-,\bp[lr]-,float-left,float-right,text-left,text-right,border-left,border-rightfont-weight-,font-italic,text-monospaceno-gutters,sr-only,embed-responsive,jumbotron,media-body,card-deck,card-columns,btn-block,thead-light,thead-dark,pre-scrollable,text-justify\$(,jQuery(near Bootstrap plugin names (.modal(,.tooltip(,.collapse()theme-color(,color-yiq(,theme-color-level(,media-breakpoint-down(
- Click through every interactive component — the data attribute renames fail silently, so a missed
data-togglelooks like a dead button rather than an error. - Check forms especially closely: custom controls, input groups, and validation feedback all changed structure.
- Then move to Step 5 and repeat this discipline for the v6 stage.
