Migrating from Bootstrap 4 and knowing the limits

The Bootstrap 4 to 5 breaking changes that actually break builds, then an honest comparison with Tailwind and plain CSS.

The breaking changes that matter

Bootstrap 4Bootstrap 5Impact
jQuery requiredNo dependencyEvery $(...).modal() call must become a constructor
col-*-offset-*offset-*-*Search-and-replace across templates
ml-* / mr-* / pl-* / pr-*ms-* / me-* / ps-* / pe-*Old classes do nothing; failures are visual, not errors
text-left / text-righttext-start / text-endSame silent failure
form-group, form-rowGrid row g-3Layout collapses if not rewritten
input-group-appendPlain children with input-group-textBroken visual grouping
media componentRemovedUse flex utilities directly
jumbotronRemovedRebuild with a padded container
data-* togglesdata-bs-* togglesAll JavaScript silently stops
card-deck, card-columnsGrid with row-cols-*Rebuild the row
# A first pass that finds the worst offenders before you open a browser
grep -rEo 'data-(toggle|target|dismiss|slide|ride|spy|parent)=' src/ | sort | uniq -c
grep -rEo '\b(ml|mr|pl|pr)-[0-5a]' src/ | sort | uniq -c
grep -rnE '\$\(.*\)\.(modal|collapse|tooltip|dropdown|tab)\(' src/

# and a safety net while you migrate
npm install bootstrap@5 bootstrap-icons
  • Rename every data-* attribute to data-bs-* first: it is mechanical and unblocks the interactive components immediately.
  • Then fix the direction-aware spacing classes. These fail silently, so a visual diff of each page is the only reliable check.
  • jQuery calls have to be rewritten one by one; keep jQuery loaded during the transition so unconverted code still runs.
  • popper.js v1 must go β€” Bootstrap 5 needs @popperjs/core, and leaving the old one in a bundle produces confusing positioning bugs.

Running the migration

// Before (Bootstrap 4 + jQuery)
// $('.modal').modal('show');
// $('#collapse').collapse('hide');

// After (Bootstrap 5, no jQuery)
import { Modal, Collapse } from 'bootstrap';

Modal.getOrCreateInstance(document.querySelector('#confirm')).show();
Collapse.getOrCreateInstance(document.querySelector('#panel')).hide();

// Delegated equivalents for markup injected later
document.addEventListener('click', (event) => {
  const modalTrigger = event.target.closest('[data-bs-toggle="modal"]');
  if (modalTrigger) {
    Modal.getOrCreateInstance(document.querySelector(modalTrigger.dataset.bsTarget)).show();
  }
});
  1. Pin the version in your lockfile and load Bootstrap 5's CSS alongside Bootstrap 4's for one release, on a staging environment only.
  2. Convert the CSS layer first β€” classes and markup β€” while the old JavaScript still drives behaviour.
  3. Convert the JavaScript layer second, page by page, deleting the jQuery reference only when the last call is gone.
  4. Audit the compiled bundle: dropping jQuery plus Popper v1 typically removes 40-60 KB gzipped on its own.
  5. Keep a visual regression baseline per page; most Bootstrap 5 regressions are spacing and alignment, which automated tests will not catch.
πŸ’‘
Bootstrap 5 ships an RTL stylesheet and a colour-mode system that Bootstrap 4 never had. If you are migrating anyway, budget the time to check both β€” retrofitting dark mode later means revisiting every custom colour you wrote during the migration.

When Bootstrap is the wrong choice

FactorBootstrapTailwindPlain CSS
Design freedomLooks like Bootstrap unless you work at itUnlimited, if the team is fluentUnlimited
Time to first pageVery fastFast with familiaritySlow
Bundle cost~30 KB CSS gzipped, tree-shakeable via SassOnly what you useOnly what you write
Component behaviourModals, dropdowns, tabs includedHeadless libraries neededYou build or vendor it
Accessibility baselineGood defaults, ARIA wiredDepends entirely on your markupDepends entirely on you
Long-term maintenanceUpgrade guides exist; class renames break thingsLow churnNo churn
Team scalingNew developers are productive immediatelyRequires conventions to stay consistentRequires architecture decisions
  • Choose Bootstrap for internal tools, admin surfaces and marketing pages where consistency beats originality β€” the components you would otherwise rebuild are the value.
  • Avoid it for a highly bespoke brand system: you will spend the effort fighting specificity and end up overriding more than you write.
  • Bootstrap is a poor fit for a very small page where a 30 KB stylesheet is more than the whole rest of the site.
  • Mixing Bootstrap with Tailwind is almost always a mistake: the two utility systems overlap and the reset rules conflict.

The honest summary: the framework saves the most time on the parts nobody enjoys writing β€” modals, dropdown focus handling, form validation states, a grid that does not break at odd widths. Its cost is a recognisable look and an upgrade path with real breaking changes. Decide which side is larger for the specific project instead of arguing in the abstract.

FAQ

Can I run Bootstrap 4 and 5 on the same page?
Technically yes if you scope one with a prefix, but the two grid systems and resets will collide in ways that are painful to debug. Migrate one page at a time behind a route split rather than mixing both across a whole site.
Is Bootstrap 5 worth migrating to just for the jQuery removal?
Only if jQuery is the largest thing in your bundle or you already have to touch the code. If the site works and nothing else is planned, the migration cost β€” mostly silent class renames β€” often exceeds the payload saving.

Utilities and Sass customisation The grid system

Last refreshed 2026-09-18.