21 KiB
Bootstrap v5 → v6: analysis and a phased plan for Tabler 2.0
Status: analysis only, nothing implemented. Written 2026-09-06.
Source of truth: local Bootstrap checkout at ~/htdocs/bootstrap, branch v6-dev
(6.0.0-alpha1, 495 commits ahead of main/5.3.8, work from 2025-08-26 to 2026-07-22).
Primary reference: site/src/content/docs/guides/migration.mdx (539 lines).
Upstream also ships skills/bootstrap-v5-v6-migration, a phase-by-phase migration skill for
coding agents — worth reading as a ready-made checklist.
Diff size: scss/ 150 files, +11 585 / −8 889. js/src/ 36 files, +6 208 / −2 149.
Tabler's exposure: core/scss/bootstrap/ holds ~5 000 lines of vendored upstream Sass and
core/js/src/bootstrap/ ~4 900 lines of a vendored TypeScript port of the v5 plugins.
1. What actually changed upstream
1.1 Class naming moved from infix to prefix
Every responsive and state class now uses a Tailwind-style prefix:class pattern.
| v5 | v6 |
|---|---|
.col-md-6 |
.md:col-6 |
.d-md-none |
.md:d-none |
.navbar-expand-md |
.md:navbar-expand |
.offset-md-2, .g-md-3 |
.md:offset-2, .md:g-3 |
.container-sm |
.sm:container |
.table-responsive-md |
.md:table-responsive |
.list-group-horizontal-md |
.md:list-group-horizontal |
.sticky-md-top |
.md:sticky-top |
.opacity-50-hover:hover |
.hover:opacity-50 |
.d-print-none |
.print:d-none |
.dialog-fullscreen-sm-down |
.sm-down:dialog-fullscreen |
In Sass, breakpoint-infix() became breakpoint-prefix() and returns "md\:";
loop-breakpoints-up / loop-breakpoints-down expose $prefix instead of $infix.
Breakpoints themselves moved: lg 992 → 1024, xl 1200 → 1280, xxl renamed to 2xl and
1400 → 1536. Containers: xl 1140 → 1200, 2xl 1320 → 1440. $grid-breakpoints → $breakpoints.
1.2 Sass architecture
_variables.scss,_variables-dark.scssand_maps.scssare gone. New files:_config.scss(all variables),_colors.scss,_theme.scss, and a rewritten_root.scss.- CSS cascade layers declared in
_root.scssand applied across every partial. Upstream hit the same limitation we did: Sass will not allow@use/@forwardinside@layer. $prefixwas removed from Sass. Custom properties are authored unprefixed and prefixed at build time bypostcss-prefix-custom-properties— the same approach Tabler already shipped in 1.5 (.build/build-css.ts).- Node Sass support dropped. RFS removed entirely (
scss/vendor/_rfs.scssdeleted); responsive type now usesclamp(). - New directory grouping with
index.scssper group:scss/layout/,scss/content/,scss/buttons/,scss/forms/,scss/mixins/,scss/helpers/. - All
$*-focus-box-shadowvariables removed in favour of onefocus-ring()mixin driven by--focus-ring*custom properties. $enable-dark-moderemoved — dark mode is always compiled, switched at runtime viadata-bs-theme.
1.3 Colour system
- Every colour variable is
oklch(); tint/shade scales are generated withcolor-mix(in lab, …)in the compiled CSS. - All
--*-rgbcustom properties andrgba()patterns are gone. This is a hard browser requirement onoklch()andcolor-mix(). - A
.theme-*class sets--theme-bg,--theme-fg,--theme-border,--theme-contrastand components only read those tokens. Per-colour component classes are replaced by composition:.btn-primary→.btn-solid .theme-primary.btn-outline-primary→.btn-outline .theme-primary.badge.bg-primary→.badge-subtle .theme-primary- the same for alerts, tables, cards, accordions and pagination
- New button variants:
.btn-solid,.btn-outline,.btn-subtle,.btn-text,.btn-styled,.btn-icon, plus a.btn-xssize. Buttons and inputs share--btn-input-*sizing tokens.
1.4 Components rebuilt on native browser APIs
This is the substantive part of the release, not the renames.
- Modal → Dialog on the native
<dialog>element withshowModal()/show()/close(). The.modal-dialog/.modal-contentwrappers and the.modal-backdropelement are gone (::backdropinstead), and with them the internalutil/backdrop,util/focustrapandutil/scrollbarhelpers — the browser supplies the backdrop, the focus trap and the inert top layer. Scroll lock is:root.dialog-openplusscrollbar-gutter: stable. - Offcanvas → Drawer, sharing a
DialogBaseclass with Dialog, plus swipe-to-dismiss and a.drawer-sheetvariant. - Responsive navbar uses Drawer, not Collapse. The toggler targets a
<dialog class="drawer">..navbar-light/.navbar-darkremoved. - Accordion on
<details>/<summary>, no JavaScript for open/close. Exclusive groups use the HTMLnameattribute instead ofdata-bs-parent..accordion-buttonand.accordion-collapseare gone. - Carousel on CSS scroll snap. No more
float+translateXor a custom swipe handler. New multi-slide, peek, gap and centre modes.ride→autoplay(boolean, opt-in),wrap→ends: loop|wrap|stop. Removed.carousel-caption,.carousel-dark,.carousel-control-prev/nextand the transitional.carousel-item-*classes. - Dropdown → Menu.
.dropdown-menu→.menu,.dropdown-item→.menu-item,data-bs-toggle="dropdown"→"menu", events*.bs.dropdown→*.bs.menu. The.dropdownwrapper and.dropdown-toggleclass are gone and markup flattens from<ul><li><a>to<div class="menu"><a class="menu-item">. Direction comes fromdata-bs-placement. - Icons (close button, navbar toggler, breadcrumb divider, carousel controls, checkbox marks) are
drawn with
mask-imagetinted bycurrentcolorinstead of embedded SVG or filters..btn-close-whiteand$btn-close-white-filterremoved. .card-groupand.*:list-group-horizontalswitched from media queries to container queries — without a query container (e.g..contains-inline) on a parent they stay stacked.
1.5 JavaScript
- ESM only. No UMD bundles, no
window.bootstrapglobal;<script type="module">is required. Data attribute APIs are unchanged. - Popper replaced by Floating UI (
@floating-ui/dom, a peer dependency). ThepopperConfigoption is nowfloatingConfig. - Vanilla Calendar Pro added as a peer dependency for the new Datepicker.
- ScrollSpy rewritten on
IntersectionObserverwith an activation line (topMargin, default12%); the deprecatedoffsetandmethodoptions are gone. - New plugins:
Menu,Dialog,Drawer,Combobox,Chips,OtpInput,Strength,Range,Toggler,Datepicker,NavOverflow.
1.6 Utilities — the quiet breakage
Class names survive but their values change, so old markup compiles and silently looks wrong.
$spacersgrew from 0–5 to 0–9 and remapped: v53(1rem) is v64; v54(1.5rem) is v66; v55(3rem) is v69..rounded-Nregenerated from a numeric$radiimap (--radius-0…--radius-9); v5.rounded-1is roughly v6.rounded-3, v5.rounded-4is v6.rounded-8..fs-1….fs-6replaced by ascending t-shirt sizes.fs-xs….fs-6xl;.lh-base→.lh-md;.display-1….display-6and.leadremoved with no single-class replacement..text-*colour utilities →.fg-*;.text-bg-*,.bg-light,.bg-darkand.bg-body-*removed in favour of a neutral.bg-1….bg-4scale;.text-reset→.fg-reset..mh-*/.mw-*→.max-h-*/.max-w-*; new.min-h-*/.min-w-*.- Negative margins reduced to
.ms--1,.ms--2,.me--1,.me--2. - Spacing and border utilities now emit logical properties (
margin-block-start,padding-inline-start,border-inline-end) with unchanged class names. - Layered, themeable shadows:
.shadow-xs….shadow-xl,.shadow-{color},.shadow-opacity-*. link-*utilities folded intounderline-*.- New
.contains-inline/.contains-sizecontainer-query utilities,.grid-cols-*,.place-items,.justify-items,.border-keyline(0.5px).
1.7 Forms
.form-selectremoved — use.form-controlon<select>._form-check.scsssplit into_checkbox/_radio/_switch;.checkgoes directly on the<input>with no wrapper..btn-checkmoves to the<label>with a nested input, using:has()instead ofid/forpairs.- Validation rewritten: no
.was-validated, no bare:valid/:invalid. Opt in withdata-bs-validateon the<form>and style:user-invalid.$enable-validation-iconsand all background validation icons removed.$form-validation-states→$validation-states. .form-rangebecomes a wrapper; the input takes.form-range-inputand the component is JavaScript-driven.- New
.form-field/.form-groupgrid layout primitive (label + control + help + feedback). Input groups loseflex-wrapand.has-validation.
1.8 Bootstrap moved onto Tabler's turf
v6 adds components Tabler already ships under the same names: avatar (core/scss/ui/_avatars.scss),
chip (_chips.scss), stepper (Tabler's _steps.scss), plus progress and status work.
It also adds ones Tabler does not have: otp-input, datepicker, combobox, prose,
form-adorn, password strength, nav-overflow, form-field.
The class names collide while the APIs and tokens do not. Before 2.0 we have to decide, per
component, whose definition of .avatar, .chip and .stepper wins.
2. Fit with the agreed 2.0 direction
The decision on record for 2.0 is: our own core, v5 class names with v6 architecture, no wholesale port of v6. Against that, the upstream diff splits fairly cleanly.
Take:
_config / _colors / _theme / _root split with the defaults() + tokens() pattern ·
cascade layers · PostCSS prefixing (already shipped in 1.5) · one focus-ring() mixin ·
logical properties · native <dialog> and <details> (removes JavaScript and fixes several
findings from our a11y audit) · Floating UI (already in progress on
migrate-popper-to-floating-ui) · the $radii scale · scroll-snap carousel.
Skip or defer:
the md: prefix syntax · the modal→dialog, offcanvas→drawer, dropdown→menu renames
(take the implementations, keep the v5 names) · the $spacers and .rounded-* remapping ·
.text-* → .fg-*.
Cannot be taken halfway:
oklch() + color-mix() and the death of --*-rgb — they are the foundation of the .theme-*
system and every colour variant, so adopting the v6 token architecture means adopting the browser
requirement too. Same for ESM-only JavaScript.
3. Phased migration plan
Phases are ordered by dependency. All 2.0 work targets the long-lived v2-dev branch (already on
origin); each phase lands as its own feature branch cut from v2-dev and merged back by PR, never
into dev, which is frozen for 1.x. Every phase that
claims to be output-neutral must be proved with the html-diff harness; every phase that changes
class emission must go through check-markup-classes.
Phase 0 — Decisions and prerequisites
No code. Settle the things that later phases cannot proceed without.
- Browser baseline: are
oklch()andcolor-mix()in? This gates phases 2 and 3. - ESM-only distribution: does 2.0 drop the UMD
tabler.jsbundle? This gates phase 5. - Class-name policy: confirm v5 names stay, and write down the exceptions we are willing to make.
- Component namespace policy for
.avatar/.chip/.stepper(see phase 9). - Read
~/htdocs/bootstrap/skills/bootstrap-v5-v6-migration/SKILL.mdand lift anything reusable into our own migration notes.
Deliverable: a short decision record; 2.0 upgrade guide skeleton in UPGRADE.md; this document
committed on v2-dev (it currently sits there untracked).
Phase 1 — Sass module architecture
Restructure without changing a single byte of compiled CSS.
- Split
core/scss/_variables.scss,_variables-dark.scssand_maps.scssinto_config.scss,_colors.scss,_theme.scss, reworking_props.scss/_rootaccordingly. - Add
index.scssper directory (ui/,forms/,mixins/,helpers/,layout/) and routetabler.scssthrough them. - Introduce the
defaults()+tokens()pattern so compile-time and runtime overrides are one mechanism. - Keep
@layerout of this phase — it is a separate, previously withdrawn prototype.
Files: core/scss/_variables.scss, _variables-dark.scss, _maps.scss, _config.scss,
_props.scss, _settings.scss, _core.scss, tabler.scss.
Gate: html-diff must show zero diff; byte-diff of core/dist/css/tabler.css.
Risk: low mechanically, high in volume. This is the phase where @use … as * writing back to
config bites, per the existing SCSS module notes.
Phase 2 — Colour system on oklch and theme tokens
The point of no return for browser support.
- Convert colour variables to
oklch(); generate tint/shade withcolor-mix(in lab, …). - Introduce
--theme-bg/--theme-fg/--theme-border/--theme-contrastand a.theme-*class family. - Remove
--tblr-*-rgband everyrgba(var(--tblr-*-rgb), …)call site. - Decide whether Tabler's colour variants keep emitting
.btn-primaryetc. as thin compositions over.theme-primary, so v5 markup keeps working.
Files: core/scss/_colors.scss (new), _theme.scss (new), _props.scss, all of
core/scss/ui/** that uses -rgb.
Gate: contrast gate from the a11y work; visual review of dark mode; html-diff will legitimately
differ here, so review it by hand.
Risk: high. The earlier standalone light-dark() / color-mix() attempt (issue #2720, branch
dev-scss-light-dark) is abandoned; this phase supersedes it, so reuse its lessons (cascade order
of color-scheme, the token-dump verification harness) but not its code.
Phase 3 — Token scales
- Adopt the numeric
$radiimap and--radius-0…--radius-9, keeping.rounded-*mapped to the v5 values so existing markup does not shift. - Decide on
$spacers: adopting the v6 0–9 scale silently changes every.p-3/.m-4in every Tabler template. Recommendation: keep the v5 scale, add finer steps at new keys. - Replace all
$*-focus-box-shadowwith onefocus-ring()mixin over--focus-ring*tokens. - Consider the layered shadow scale (
.shadow-xs….shadow-xl) and.border-keyline. - Skip the
.fs-*renaming — it inverts the scale direction and breaks every page.
Files: core/scss/_config.scss, _props.scss, core/scss/mixins/**, core/scss/_utilities.scss.
Gate: html-diff zero diff for the radius and focus-ring work.
Phase 4 — Logical properties
Already scoped on the migrate-css-physical-directions-to-logical branch. Upstream did the same,
which validates the direction.
- Emit
margin-block-*/padding-inline-*/border-inline-*from spacing and border utilities with unchanged class names. - Reconcile with the
--tblr-dirmultiplier work fortranslateX, and keep the/*rtl:ignore*/workaround for rtlcss negatingcalc().
Gate: RTL build comparison; html-diff zero diff.
Phase 5 — JavaScript runtime
- Decide ESM-only, then drop the UMD build from
core/package.json(js:build:standalone,js:min:*) or keep a compatibility bundle for one more major. - Finish the Floating UI migration and rename
popperConfig→floatingConfigin our API. - Take the ScrollSpy rewrite (
IntersectionObserver+ activation line) intocore/js/src/bootstrap/scrollspy.ts. - Prepare to delete
util/backdrop.ts,util/focustrap.ts,util/scrollbar.ts— but only after phase 6 lands, since they are what the native<dialog>replaces.
Files: core/js/src/bootstrap/** (~4 900 lines), core/js/src/tabler.ts, core/package.json,
core/.build/vite.config.mts.
Gate: core vitest suites; browser smoke test of every preview page that initialises a plugin.
Phase 6 — Native components
The phase with the best effort-to-value ratio: less code, better accessibility, same class names.
- Rebuild the modal on
<dialog>while keeping.modal/.modal-header/.modal-body/data-bs-toggle="modal". Watch for Tabler's own modal variants and thepreview/modal architecture. - Rebuild offcanvas on the same
DialogBase. - Rebuild the accordion on
<details>/<summary>;.accordion-buttondisappears, so the docs class reference and every accordion example need updating (~126 occurrences across 6 files). - Rebuild the carousel on CSS scroll snap.
- Consider the responsive navbar moving from Collapse to a drawer — but Tabler's navbar and sidebar are heavily customised, including the 1.5 folded sidebar, so evaluate this separately.
- Convert close button, navbar toggler and breadcrumb divider icons to
mask-image+currentcolor.
Files: core/scss/bootstrap/_modal.scss, _offcanvas.scss, _carousel.scss,
core/scss/ui/_modals.scss, _offcanvas.scss, _carousel.scss, _accordion.scss, _close.scss,
core/js/src/bootstrap/{modal,offcanvas,carousel,collapse}.ts, plus shared/ui/** and
preview/pages/** markup.
Gate: html-diff will differ by design — review page by page; keyboard and screen-reader passes.
Risk: high, but each component can ship independently.
Phase 7 — Forms
- Decide on
.form-select: dropping it means touching ~650 occurrences across the repo. Options are to keep it as an alias over.form-controlor to defer entirely. - Split
_form-check.scssinto checkbox / radio / switch, with.checkdirectly on the input. - Move validation to
data-bs-validate+:user-invalid; drop the built-in validation icons. - Make
.form-rangea wrapper with a JavaScript-driven filled track. - Evaluate
.form-fieldas a layout primitive against Tabler's existing form markup.
Files: core/scss/bootstrap/forms/**, core/scss/ui/forms/**, shared/ui/ form components,
docs/content/** form pages.
Risk: medium-high, and the most visible to template users.
Phase 8 — Utilities and the responsive naming question
Kept last because it is the most disruptive and the most optional.
- Container-query utilities (
.contains-inline,.contains-size) and grid utilities (.grid-cols-*,.place-items,.justify-items) are pure additions — take them. - The
md:prefix syntax is not adopted in 2.0 (decided 2026-09-06). Adopting it would mean rewriting roughly 1 800 class occurrences across ~200 files inpreview/,docs/andshared/, and breaking every third-party Tabler template. v5 infix names (.col-md-6,.d-md-none) stay. - Revisit for 3.0 at the earliest; if ever adopted, ship both spellings for one major and add a codemod.
- Breakpoint value changes (
lg1024,xl1280,2xl1536) can be considered independently of the naming, but they reflow every layout, so treat them as their own decision.
Gate: check-markup-classes is the gate that makes any of this reviewable.
Phase 9 — Component namespace collisions
For each of .avatar, .chip, .stepper (Tabler's .steps), .progress and .status, decide
whether Tabler keeps its own definition, adopts upstream's, or renames. Then decide whether to
implement v6's genuinely new components ourselves: otp-input, combobox, datepicker,
password strength, prose, form-adorn, nav-overflow, form-field.
Note that our datepicker plans and upstream's choice of Vanilla Calendar Pro may or may not agree — worth checking before committing to a library.
Deliverable: a component ownership table in the 2.0 planning notes.
Phase 10 — Documentation and gates
- Write the 2.0 upgrade guide the way 1.5's was written, with before/after for each breaking change.
- Update
classnamesfront matter acrossdocs/content/**for every class that moved. - Extend
check-markup-classesbaselines. - Add changesets per phase.
4. Suggested ordering
Phase 0 decisions
↓
Phase 1 Sass architecture ──────────────┐
↓ │
Phase 2 oklch + theme tokens │ (Phase 5 JS can run in parallel
↓ │ from the start, except for the
Phase 3 token scales │ util/* deletions, which wait
↓ │ on Phase 6)
Phase 4 logical properties ─────────────┘
↓
Phase 6 native components (per component, independently shippable)
↓
Phase 7 forms
↓
Phase 8 utilities / naming decision
↓
Phase 9 component ownership
↓
Phase 10 docs and gates
5. Open questions
- Browser baseline for
oklch()andcolor-mix()— the gate on phases 2 and 3. - ESM-only, or one more major with a UMD bundle?
.form-select: alias, removal, or defer?$spacers: keep the v5 scale, or accept the silent reflow of every template?- Who owns
.avatar/.chip/.stepperwhen upstream ships them too? - Does the v6 navbar-as-drawer pattern survive contact with the folded sidebar from 1.5?