Migrating to v6
Track and review changes to the CoreUI for Bootstrap source files, documentation, and components to help you migrate from v5 to v6.
Work in progress. v6 is under active development on the v6-dev branch —
this page tracks every migration-relevant change as it lands, so nothing here
is final until a v6 release ships. The class API keeps its meaning —
.btn-primary, .alert-success and friends render as before. Where the Sass
or CSS-variable surface does change, the entry below carries a
Breaking badge.
v6.0.0 (unreleased)
Dependencies
- Popper is gone;
@floating-ui/domreplaces it. Breaking Every positioned component — tooltips, popovers, dropdowns, autocomplete, multi-select, and the pickers — now positions with Floating UI.@popperjs/coreis no longer a peer dependency and the bundle builds (coreui.bundle.*) no longer include it. What to change:- npm: replace
@popperjs/corewith@floating-ui/domin your dependencies. - CDN (non-bundle builds): replace the
popper.min.jsscript tag with the two Floating UI files, in this order:floating-ui.core.umd.min.js, thenfloating-ui.dom.umd.min.js. - The
popperConfigoption (tooltip, popover, dropdown) is nowfloatingConfigand receives a Floating UI computePosition config — Popper’smodifiersarray does not translate; use Floating UI middleware. - The tip and menu carry
data-coreui-placementinstead of Popper’sdata-popper-placement; CSS or JS keying on the old attribute needs the new name. - Positioning is now asynchronous (
computePositionis promise-based). Code that read the tip’s coordinates synchronously right aftershow()should wait for theshownevent.
- npm: replace
Distributed files
-
The
*.rtl.cssbuilds are gone. Breaking One stylesheet now serves both directions, sodist/cssno longer carriescoreui.rtl.css,coreui-grid.rtl.css,coreui-reboot.rtl.css,coreui-utilities.rtl.css, their minified twins, source maps or SRI hashes — 20 files in all. Everything positional was already written with logical properties; the six places that genuinely differ (the off-canvas panel, the range slider’s label and tooltip, the select and validation arrows, the carousel controls, the breadcrumb divider) now emit their mirrored declaration from a:dir(rtl)rule in the same file, matched on the element’s own direction rather than on an ancestor. That costs about 500 bytes gzip, where an RTL reader used to download a second 56 kB stylesheet.What to change: drop the
.rtlfrom your stylesheet link and keepdir="rtl"on<html>.$enable-rtl: falsestill produces an LTR-only build and$enable-ltr: falsean RTL-only one, so nothing that was possible before stops being possible. -
The Bootstrap theme and the Bootstrap-compatible JavaScript are gone. Breaking
dist/css/themes/bootstrap/bootstrap.css,dist/js/bootstrap.js,bootstrap.bundle.jsandbootstrap.esm.jsno longer ship, with their minified twins, source maps and SRI hashes, and neither doesscss/themes/bootstrap/bootstrap.scss. The two halves no longer agreed in v6: the JavaScript wrotedata-bs-*attributes and--bs-*properties where the stylesheet still readdata-coreui-*and--cui-*ones.What to change: load
coreui.min.cssandcoreui.bundle.min.js(orcoreui.min.js), writedata-coreui-*attributes, listen for*.coreui.*events, reach the plugins throughcoreuiinstead ofbootstrap, and set--cui-*properties instead of--bs-*. -
dist/cssgainedbase.cssand a stylesheet per component.base.cssholds the:roottokens and Reboot;css/components/holds one file per component plusmanifest.json, which records each file’s load order and the components it needs alongside it. Nothing was taken away —coreui.cssstill carries everything — so this is additive, and pages that use a few components can now ship a few kilobytes of CSS instead of the whole library. See per-component CSS files. -
dist/cssgainedcoreui-legacy.css, andcoreui.csslost the v5 spellings it carries. Breakingcoreui.cssis the v6 class API only. The deprecated v5 spellings — the colour modifiers (.alert-{color},.btn-{color},.btn-outline-{color},.btn-subtle-{color},.btn-ghost-{color},.callout-{color},.chip-{color},.list-group-item-{color}), the form check markup (.form-check,.form-check-input,.form-check-label,.form-check-inline,.form-switch,.form-switch-lg/-xl) and.btn-checkon the input — moved tocoreui-legacy.css, 3.4 kB gzip. That takes 2.1 kB gzip offcoreui.cssfor markup that has already moved, and turns the compatibility layer into something you can see on a network tab and delete in one line.What to change: if your markup still uses any of them, add the sheet after
coreui.css(or last, afterbase.cssand the component files):<link href="css/coreui.min.css" rel="stylesheet"> <link href="css/coreui-legacy.min.css" rel="stylesheet">From Sass,
@use "coreui-legacy";next to your@use "coreui";. Everything in it is still removed in v7. See v5 compatibility CSS. -
$enable-deprecated-classesand$enable-deprecated-form-checkare gone. Breaking A separate stylesheet answers the question both flags existed for, without a recompile:falseis now “do not loadcoreui-legacy.css”. Sass builds that set either flag fail with an unknown-variable error — delete the line.
Browser support
BreakingThe minimum supported browsers are now Chrome 123, Edge 123, Firefox 129,
Safari 18 and iOS 18. Safari 18 is set by color-mix(): a mix in oklch with
an achromatic operand — transparent, white, black, the greys, which is how
every translucent and tinted colour in v6 is made — keeps the other colour’s
hue only from Safari 18, where WebKit started carrying missing components
forward as the spec requires; Safari 17.x interpolates the hue towards 0° and
renders indigo as pink. Apple ships Safari 18 for macOS 13 and 14 as well, so
the cut only affects browsers left un-updated. light-dark() sets the
Chrome/Edge floor;
@starting-style and transition-behavior: allow-discrete set Firefox 129 (the
Menu’s entry animation) and @property needs 128 — its inherits: false
registrations are what stop the colour and opacity utilities leaking into
descendants, so an older Firefox does not lose an animation, it gets a bug.
:has() sets a lower bound of Firefox 121, since validation and focus states on Multi Select,
Autocomplete and the pickers, the sidebar navigation indicators, calendar
ranges and the rating icons are all selected with it — and an unsupported :has() invalidates
the entire rule it appears in. This is the floor the v6 color system already required in practice:
theme decisions resolve in the browser via light-dark() and color-mix()
with no fallback, so on older browsers dark mode did not work regardless of
what the previous .browserslistrc claimed (Chrome 60 / Safari 12). v6 makes
the declaration honest and takes the wins: the compiled CSS drops legacy
vendor prefixes (about 8 kB gzip off coreui.min.css) and the JavaScript
ships modern syntax un-transpiled. If you must support older browsers, stay
on v5.
JavaScript
v6’s JavaScript is written in TypeScript. The published behaviour is unchanged —
the sources were converted module by module against a gate that required the
minified bundles to stay byte-identical — but the package now carries types, and
what it ships from js/ has changed.
Type declarations ship with the package
@coreui/coreui-pro now ships .d.ts files and points types at
js/dist/index.d.ts, so TypeScript consumers get the full API without
@types packages or hand-written module declarations:
import { Modal, MultiSelect } from '@coreui/coreui-pro'
const modal = new Modal('#example', { backdrop: 'static', keyboard: false })
const version: string = Modal.VERSIONEach component with options also exports its config type (ContextMenu and
Dropdown take Menu’s MenuConfig), so an options object can be typed and
reused, and takes it in the constructor and
getOrCreateInstance, so an option it
does not know is a type error; a picker also takes the options it forwards to
its calendar, its field and its time columns, except those it sets itself. Declarations are generated from
the sources, not maintained by hand, and CI type-checks a consumer file against
the emitted declarations, so the shipped types cannot drift from the code.
The js/src sources ship as TypeScript
Breaking
The package used to ship js/src/**/*.js; it now ships js/src/**/*.ts. The
documented per-plugin path is unaffected — js/dist/*.js still ships,
UMD-ready, and that is what the Webpack,
Vite and Parcel
guides tell you to import:
import Alert from '@coreui/coreui-pro/js/dist/alert'A bundler that reads the package’s exports now resolves that import to
the ES module next to the UMD file, js/dist/alert.mjs; a require() still
gets js/dist/alert.js.
Only imports that reached into the raw sources are affected:
- import Alert from '@coreui/coreui-pro/js/src/alert.js'
+ import Alert from '@coreui/coreui-pro/js/dist/alert.js'If your toolchain compiles TypeScript, you can import js/src/*.ts directly and
get types and tree-shaking together. The per-plugin files are typed as well —
js/dist/alert.js ships next to js/dist/alert.d.ts, and js/dist/alert.mjs
next to js/dist/alert.d.mts.
js/index.esm.js and js/index.umd.js are no longer published
Breaking
These two files are the bundler’s entry points, and they import the sources — which are now TypeScript, so the files could not resolve for a consumer anyway. They are no longer part of the published package. Use the built bundle instead:
- import { Tooltip } from '@coreui/coreui-pro/js/index.esm.js'
+ import { Tooltip } from '@coreui/coreui-pro/dist/js/coreui.esm.js'or, if you are already resolving the package by name, import { Tooltip } from '@coreui/coreui-pro' — module points at the same ESM bundle.
Unused date helpers are gone from js/dist/util
Breaking
The util modules are internal, but the package exports every file under js/dist, so a deep import could reach them. These helpers are no longer exported: js/dist/util/date-range-picker.js as a whole, getFirstAvailableDateInRange, getSelectableDates, createDateFromMonth, createDateFromWeek and createDateFromYear from util/calendar.js, getDateSections from util/date-sections.js (now getSectionLayout), and convertTimeToDate, formatTimePartials, getAmPm, isValidTime and shouldUseTwoDigitHour from util/time.js. Most had no reader in the library and are gone; createDateFromYear, formatTimePartials and shouldUseTwoDigitHour stay as module internals. Pass dates to the components in the formats their options take (startDate: '2026W03' with selectionType: 'week'), or keep a copy of the helper in your code. The nine per-unit comparisons of util/calendar.js, isMonthDisabled, isMonthInRange, isMonthSelected and their isQuarter… and isYear… counterparts, are three functions that take the calendar view as the second argument: isPeriodDisabled(date, 'months', min, max, disabledDates), isPeriodInRange(date, 'quarters', start, end) and isPeriodSelected(date, 'years', start, end).
CalendarUtil and RangeUtil in the per-file UMD builds
Loaded with a plain <script>, js/dist/util/calendar.js registers as CalendarUtil, and the new js/dist/util/range.js as RangeUtil. The calendar util used the Calendar global of the component, so the two files overwrote each other and js/dist/calendar.js read its helpers from itself. Load the util file before the component, as with the other js/dist/util files.
coreui.Offcanvas in the UMD bundle
The UMD bundle exposed the offcanvas plugin as coreui.OffCanvas, while the
ESM bundle, the per-plugin file and the docs all call it Offcanvas. The UMD
global now carries coreui.Offcanvas as well; coreui.OffCanvas stays as an
alias of the same class, so existing scripts keep working:
- const offcanvas = new coreui.OffCanvas('#offcanvasExample')
+ const offcanvas = new coreui.Offcanvas('#offcanvasExample')The UMD bundle keeps what companion packages put on window.coreui
coreui.js and coreui.bundle.js used to replace window.coreui with a
fresh object, so coreui.Utils, coreui.ChartJS, coreui.DataGrid or
coreui.Scheduler loaded before the library were gone once it loaded. The
library now copies the entries of an existing plain window.coreui into its
own object, so the order of the script tags no longer matters. Anything else
under that name, such as an element with id="coreui", is still replaced.
One jQuery dispatcher for every plugin
Every $(el).plugin('method') call now goes through the same dispatcher:
the instance is created or reused, a string argument names a public
method, and anything else throws TypeError: No method named "…". A few
plugins used to let a private name through ($(el).tooltip('_getConfig')
called it) or skipped the check altogether (button, number-input,
password-input); they now reject those names like the rest.
The attribute that enables a plugin
data-coreui-toggle="…" now means one thing only: this element is a control
the user activates. It stays on the <button>, <a> or <input> that opens,
closes or presses something — Modal, Offcanvas, Dialog, Drawer, Collapse, Tab,
Tooltip, Popover, Context Menu, Combobox, Button, Loading Button.
Everywhere else the element is a mount point or a source: the plugin builds the
component inside or around it, and nothing is toggled. Those take a bare
data-coreui-<name>, the shape Chip Input, Chip Set, Multi Select and the
masked date and time fields already used. Renamed:
- <div data-coreui-toggle="date-picker"></div>
+ <div data-coreui-date-picker></div>| was | is |
|---|---|
data-coreui-toggle="autocomplete" | data-coreui-autocomplete |
data-coreui-toggle="calendar" | data-coreui-calendar |
data-coreui-toggle="date-picker" | data-coreui-date-picker |
data-coreui-toggle="date-range-picker" | data-coreui-date-range-picker |
data-coreui-toggle="list-box" | data-coreui-list-box |
data-coreui-toggle="number-input" | data-coreui-number-input |
data-coreui-toggle="otp" | data-coreui-otp |
data-coreui-toggle="password-input" | data-coreui-password-input |
data-coreui-toggle="password-strength" | data-coreui-password-strength |
data-coreui-toggle="range-slider" | data-coreui-range-slider |
data-coreui-toggle="rating" | data-coreui-rating |
data-coreui-toggle="stepper" | data-coreui-stepper |
data-coreui-toggle="time-picker" | data-coreui-time-picker |
data-coreui-toggle="transfer" | data-coreui-transfer |
The JavaScript constructors are unchanged — this is the markup entry point only, and only in the vanilla package.
data-coreui="navigation" is gone. The sidebar navigation read two
attributes for one job; the v4-era spelling was kept for compatibility and is
now dropped. Use data-coreui-navigation, which v5 already documented as the
one to write.
Lifecycle methods return a promise
show(), hide(), toggle() and close() returned before the transition
ended, so continuing after one meant listening for shown.coreui.* /
hidden.coreui.*. They now return a promise that resolves at the same moment
that event fires, so you can await the method instead:
- myCollapseEl.addEventListener('shown.coreui.collapse', () => {
- // the area is expanded
- })
- collapse.show()
+ await collapse.show()
+ // the area is expandedCollapse, Menu, Modal, Offcanvas, Popover, Sidebar and Tooltip return one from
show/hide/toggle, Toast from show/hide, Tab from show, and Alert
from close. The event path is unchanged and still the right tool when you must
react to changes your own code did not start.
The promise always settles — a call that does nothing (already open, a prevented
show.coreui.*, an instance disposed mid-transition) resolves right away rather
than stranding the caller. It resolves with no value, so it tells you the call
finished, not whether the state changed.
Live changes go through setters, not update(config)
Breaking
update() meant two different things. On Combobox, List box, Transfer, Menu,
Progress, Range, Tooltip and Popup it takes no argument and recomputes from what
the component already owns; on Autocomplete, Calendar, MultiSelect, OTP Input,
Range Slider, Rating and the date/time inputs it took an options object and
replaced the configuration. Calling it the first way on a component that meant
the second one threw a TypeError.
Options are now fixed once a component starts. What can change on a live instance, its value, its data and its state, has a method of its own, named after the option it changes:
- multiSelect.update({ options })
+ multiSelect.setOptions(options)
- rating.update({ value: 2 })
+ rating.setValue(2)
- dateInput.update({ validationState: 'invalid' })
+ dateInput.setValidationState('invalid')
- autocomplete.update({ disabled: true })
+ autocomplete.disable()| Component | Methods |
|---|---|
| Autocomplete | setValue(), setOptions(), setValidationState(), disable(), enable() |
| Calendar | setStartDate(), setEndDate(), setRange(), setCalendarDate(), setSelectEndDate() |
| Chip Input, Combobox | setValidationState(); Combobox keeps setValue() and setItems() |
| Date Input, Time Input | setDate(), setValidationState(), disable(), enable() |
| Date Range Input, Date Range Picker | setStartDate(), setEndDate(), setRange(), setValidationState() |
| Date Picker, Time Picker | setDate() / setTime(), setValidationState() |
| Multi Select | setValue(), setOptions(), setTotal(), setValidationState(), disable(), enable() |
| OTP Input, Range Slider, Rating | setValue(), setValidationState(), disable(), enable() |
- A setter takes the type of its option. A wrong type throws the
TypeErrorthe constructor throws, and the component is left as it was; Autocomplete and MultiSelect skipped that check onupdate(). - The date and time setters fire no change event, since the code that
calls them already holds the value.
setDate()of Date Input, Time Input and Date Picker,setTime()of Time Picker,setStartDate(),setEndDate()andsetRange()of Date Range Input, Date Range Picker and Calendar, andsetCalendarDate()andsetSelectEndDate()of Calendar change the value withoutdateChange,timeChange,startDateChange,endDateChange,calendarDateChange,selectEndChangeor the nativeinputandchangeof the value field;errorChangestill reports a date the field refuses. The verbs (clear(),reset(),today(),now()) and what the user does fire as before.setDate(date, true)of Date Input and Time Input and the same last argument of the Date Range Input setters fire the events typing the date would, for a date the user gives through another control. - Other options are fixed. A new format, size, label, icon,
name,requiredorreadonlymeans disposing the instance and creating a new one.update()applied some of them, such asformaton Date Input orsizeon Rating, and ignored the rest in silence on Combobox, Chip Input, Date Range Input and the pickers. - A setter keeps what it does not set.
setValue()keeps a given validation state,setValidationState()keeps the value, and Rating and Range Slider no longer fall back to the defaults:rating.setValue(2)on a rating built with{ itemCount: 3 }keeps three items. - The setters are reachable the same way everywhere, including through the
static and jQuery dispatchers, which pass the method name and whatever
follows it:
coreui.Rating.ratingInterface(el, 'setValue', 2).
Toaster.update(id, options) is unchanged — it rewrites one toast in the store
rather than reconfiguring the component.
A getter says what it returns
BreakinggetSelected() returned three different things. List box gave the selected
values, Chip Set gave the selected elements while getSelectedValues() sat
next to it giving the values, and Transfer required an argument no other
component takes:
chipSet.getSelected().join(',') // [object HTMLDivElement],[object HTMLDivElement]
transfer.getSelected() // TypeError: Cannot read properties of undefinedEvery getter now carries the noun it returns:
- listBox.getSelected()
+ listBox.getSelectedValues()
- chipSet.getSelected()
+ chipSet.getSelectedValues()
- transfer.getSelected('source')
+ transfer.getSelectedValues('source')Chip Set’s getValues() and getSelectedValues() are unchanged and are now
the whole selection API: the element list behind them is internal. A chip
carries a value of its own — data-coreui-chip-value, falling back to the
chip’s text — so values, not nodes, are what the rest of the plugin works
with.
Transfer still takes 'source' or 'target', the argument its
setLoading(loading, list) already takes. Anything else now throws a
TypeError naming the two lists instead of failing on an undefined property.
The button that opens a panel is pickerIcon
Breaking
indicator named two unrelated things. On Autocomplete it was a boolean that
showed the button opening the options menu; on List box, Combobox and Transfer
it is a string choosing the marker drawn on each option. The button also
answered to three different names across the family — indicator,
indicatorIcon, and ariaIndicatorLabel next to ariaToggleLabel.
The word stays where it describes something that indicates — the per-option
marker — and the button takes the name the platform gives it
(::picker-icon,
::picker(select), showPicker()):
- new coreui.Autocomplete(el, { indicator: true })
+ new coreui.Autocomplete(el, { pickerIcon: true })
- new coreui.DatePicker(el, { indicatorIcon: mySvg, ariaToggleLabel: 'Open' })
+ new coreui.DatePicker(el, { pickerIcon: mySvg, ariaPickerLabel: 'Open' })
- new coreui.MultiSelect(el, { optionsStyle: 'text' })
+ new coreui.MultiSelect(el, { indicator: 'none' })| was | is | on |
|---|---|---|
indicator: boolean | pickerIcon: string | boolean | Autocomplete |
indicatorIcon | pickerIcon | the four pickers |
ariaIndicatorLabel | ariaPickerLabel | Autocomplete, Multi Select |
ariaToggleLabel | ariaPickerLabel | the four pickers |
optionsStyle: 'checkbox' | 'text' | indicator: 'checkbox' | 'none' | Multi Select |
pickerIcon takes over both jobs the old pair did: false renders no button,
true renders the built-in icon, and a string renders your own inline SVG
(sanitized). Autocomplete and Multi Select gained it — their icon used to be
hard-coded — so pickerIcon: false now hides Multi Select’s button too.
Unchanged: indicator on List box, Combobox and Transfer, along with the
.list-box-option-indicator class and the data-coreui-indicator attribute;
Password Input’s ariaToggleLabel, because that button toggles visibility
rather than opening a picker; and the shared .form-control-action class,
which eight components use for three different jobs.
The “N selected” text is one option, and it can be a function
BreakingFour components rendered the same sentence under three option names and two
shapes: Combobox substituted {count} into selectedText, List box and
Transfer appended selectedCounterText after count/total, and Multi Select
appended selectionTypeCounterText after the count.
Appending cannot be translated. A translator receives only the tail of the sentence, so the number cannot move and the noun cannot be inflected — Polish needs “2 zaznaczone”, “5 zaznaczonych” and “1 zaznaczony” from the same option, and a language that puts the number last cannot be served at all.
- new coreui.ListBox(el, { selectedCounterText: 'selected' })
+ new coreui.ListBox(el, { selectedLabel: (count, total) => `${count}/${total} selected` })
- new coreui.Combobox(el, { selectedText: '{count} selected' })
+ new coreui.Combobox(el, { selectedLabel: (count) => `${count} selected` })
- new coreui.MultiSelect(el, { selectionTypeCounterText: 'item(s) selected' })
+ new coreui.MultiSelect(el, { selectedLabel: (count) => `${count} item(s) selected` })| was | is |
|---|---|
selectedText (Combobox) | selectedLabel |
selectedCounterText (List box, Transfer) | selectedLabel |
selectionTypeCounterText (Multi Select) | selectedLabel |
The string form stays, because a function cannot be written into
data-coreui-selected-label: {count} and {total} are replaced in it, so
markup-only pages keep a translatable counter.
The argument order matches the two function options the library already ships —
Rating.ariaLabel(value, itemCount) and OTPInput.ariaLabel(index, total).
total is passed everywhere, including the two components whose default text
ignores it, so one function can serve all four.
One selection event, one payload key
BreakingSeven components told you the selection had changed, under two event names and
four payload keys. Autocomplete and Multi Select fired changed, everyone else
fired change; the values arrived as value, selected, values, or as the
pair sourceValues/targetValues. Nothing announced the difference, so code
moved between two components of the same family read undefined:
// Multi Select rewritten as a Combobox — the listener never runs
el.addEventListener('changed.coreui.combobox', event => save(event.value))
// List box — the event fires, the value is undefined
el.addEventListener('change.coreui.list-box', event => save(event.value))The verb is now change everywhere and the values arrive as event.value:
- el.addEventListener('changed.coreui.autocomplete', event => save(event.value))
+ el.addEventListener('change.coreui.autocomplete', event => save(event.value))
- el.addEventListener('changed.coreui.multi-select', event => save(event.value))
+ el.addEventListener('change.coreui.multi-select', event => save(event.value))
- el.addEventListener('change.coreui.list-box', event => save(event.selected))
+ el.addEventListener('change.coreui.list-box', event => save(event.value))
- el.addEventListener('change.coreui.chip-set', event => save(event.values))
+ el.addEventListener('change.coreui.chip-set', event => save(event.value))
- el.addEventListener('change.coreui.transfer', event => save(event.targetValues))
+ el.addEventListener('change.coreui.transfer', event => save(event.value))| was | is |
|---|---|
changed.coreui.autocomplete, changed.coreui.multi-select | change.coreui.* |
event.selected (List box) | event.value |
event.values (Chip Set, Chip Input) | event.value |
event.targetValues (Transfer) | event.value |
event.sourceValues (Transfer) | event.source |
event.value carries what that component’s own getter returns, which is the
one thing the key never told you before: getValue() for Autocomplete and
Combobox, the selected values for List box (an array even in single mode),
the selected options for Multi Select, the chip values for Chip Set and Chip
Input. Transfer’s value is its target list, with the source list alongside as
event.source; event.target stays the .transfer element.
Two events keep their own names because they answer a different question.
select.coreui.chip-set still carries selected — membership and selection are
separate in a chip set — and move/moved.coreui.transfer still carry
direction and the values that are moving, which is the change itself rather
than the state after it.
On a page that loads jQuery, the two renamed events now reach onchange
handlers written as a DOM property or an inline attribute, on the element or
any ancestor: jQuery’s own trigger drops the namespace for that one path.
Measured in Chromium, a document.onchange handler goes from zero calls to one
per selected option. The five components that already fired change have
always behaved this way. Handlers bound with $(el).on('change') are not
affected, and neither is addEventListener.
changed.coreui.range keeps its name. It is not a selection event: Range
relays the value of the native <input type="range"> it decorates, on every
native input and change. Range Slider, the component of this family that
does have a selection, already fires change.
announce() (new)
announce(message, { context, priority, timeout }) reads a message to screen
reader users through a visually hidden live region of the page, or of the open
modal dialog, native or marked with aria-modal="true" like the panel of a
date picker, since a modal dialog leaves the rest of the page inert. priority
takes the values of ariaNotify(), 'normal' and 'high'. Toaster
announces its toasts through it. See
Accessibility.
Components
Accordion on the native <details>
-
Breaking The accordion renders on the native
<details>/<summary>elements now..accordion-itemgoes on a<details>element and.accordion-headeron its<summary>; the browser opens and closes the panel, so the component needs no initialization and works on markup added after load. -
Breaking
.accordion-button,.accordion-collapseand.collapsedare gone. The trigger is the<summary>itself and the panel is the element’s own content, revealed through::details-content. Style the open state with.accordion-item[open] > .accordion-headerinstead of:not(.collapsed). -
Breaking Grouping moves from
data-coreui-parentto thenameattribute. Give the<details>elements of one accordion the samenameto keep a single item open at a time; omit it for an always-open accordion. Theidplumbing (data-coreui-target,aria-controls,aria-expanded) is no longer needed — the browser exposes the disclosure state on its own. -
A
<summary>is not a heading. Where the accordion divides a page into sections, wrap the label in an<h2>–<h6>inside the summary; the heading inherits the summary’s typography. -
The header’s focus ring draws 1px inside the item (
--cui-focus-ring-offset: -1pxon.accordion-header:focus-visible), so it no longer overlaps the borders of adjacent items. -
The panel animation is CSS-driven through
interpolate-size: allow-keywords, which only Chromium implements today.accordion.jssupplies the same animation in Firefox and Safari; that part of it goes away, without a breaking change, once they shipcalc-size(). The component’s events and methods do not depend on it and stay either way. -
Breaking The accordion’s events replace Collapse’s.
shown/hidden.coreui.accordionfire on the.accordion-itemwhenever its open state changes, mirroring the nativetoggleevent but bubbling, so they can be delegated. There is noshow/hidecounterpart:<details>has no cancelable event before the state change, so an item is kept shut by cancelling the click on its<summary>instead of by preventing an event. -
Breaking The plugin replaces Collapse’s, and it is instantiated on the
.accordioncontainer, not on a panel. Where v5 calledCollapse.getOrCreateInstance(panel).show(), v6 callsAccordion.getOrCreateInstance(accordion).show(item), with the item given as an element or as its index.hide()andtoggle()take the same argument, andshowAll()/hideAll()open and close the whole accordion — the latter pair also handles thenamejuggling the HTML Standard requires, which is why expanding an exclusive accordion is a plugin call rather than a loop overopen. None of it is needed to open and close items; the element does that on its own. -
The header draws its focus ring on
:focus-visible, not:focus. A mouse click no longer paints a ring, keyboard focus still does, matching.btn. -
New CSS variables:
--cui-accordion-active-border-color(the open item’s border and the header’s inset shadow, previously stuck on the resting border colour), plus--cui-accordion-content-durationand--cui-accordion-content-easing, which drive the panel transition and are read back byaccordion.jsso both paths animate for the same length of time. -
The indicator can be an element instead of a background image. Put an
<svg class="accordion-icon">in the<summary>and the header stops drawing its own icon, so any icon set or<img>can be used without URL escaping an SVG into a custom property.--cui-accordion-header-active-iconselects the shape shown while the item is open, and a second element marked.accordion-icon-activedoes the same in markup — that is what a plus/minus accordion needs, where the icon is swapped rather than rotated. The default chevron and its rotation are unchanged when neither is used. -
Breaking The open item is neutral, and
.theme-{color}is what tints it.--cui-accordion-active-bgand--cui-accordion-active-colorwere fixed to--cui-primary-bg-subtle/--cui-primary-fg-emphasis, so every accordion turned the brand colour on open whether or not that suited the page. They are--cui-bg-2and--cui-fg-bodynow, and the active tokens resolve through--cui-theme-fg/-bg-subtle/-borderfirst — so the colour is a decision you make per accordion instead of one baked into the component. To keep the old look, put.theme-primaryon the.accordion. -
Breaking
--cui-accordion-inner-border-radiusis gone; the header’s corners follow--cui-accordion-border-radius. The token was computed from the global--cui-border-radiuswhen the stylesheet was built, so setting--cui-accordion-border-radiusrounded the item while the header inside kept the page’s default radius. The two corners are subtracted where they are used —calc(var(--cui-accordion-border-radius) - var(--cui-accordion-border-width))— so one token now shapes the whole item. Defaults render as before; drop any override of the removed variable. -
Breaking The header tokens are named after the header. The summary is not a button any more, so
--cui-accordion-btn-*is--cui-accordion-header-*(-color,-bg,-icon,-active-icon,-icon-width,-icon-color,-active-icon-color,-icon-transform,-icon-transition) and the$accordion-header-*Sass variables are$accordion-header-*. Rename in place; nothing else about them changed. -
One padding token and one font size for the whole control.
--cui-accordion-padding-x/-yand--cui-accordion-font-sizeare read by the header and the body alike, so resizing an accordion is one declaration instead of four.--cui-accordion-header-padding-*(was-btn-padding-*) and--cui-accordion-body-padding-*are still there for resizing one part, declared on.accordionasvar(--cui-accordion-padding-*). Because they resolve where they are declared, override--cui-accordion-padding-*on.accordion(or a.accordion-*size class), not on a single.accordion-item— there it no longer reaches the header and the body.
Breadcrumb
- Breaking
The breadcrumb has no bottom margin.
.breadcrumbsat onmargin-bottom: 1remthrough$breadcrumb-margin-bottomand--cui-breadcrumb-margin-bottom; both are gone and the list ends where its content ends, like every other navigation component. Add.mb-5(or setmargin-bottomon the<nav>) where the page relied on that gap. - Three breadcrumb tokens have real defaults.
--cui-breadcrumb-bg,--cui-breadcrumb-border-radiusand--cui-breadcrumb-font-sizewerenullin Sass, so the first two were emitted empty (--cui-breadcrumb-bg: ;) and the third was not emitted at all. They aretransparent,var(--cui-radius-5)andinheritnow. Nothing renders differently on a default breadcrumb, and a font size set on an ancestor still reaches it — the one visible change is that a breadcrumb given a background now has rounded corners like every other surface. Set--cui-breadcrumb-border-radius: 0to square it off. --cui-breadcrumb-divideris now declared on.breadcrumb, so set it there and not on a wrapper. The property was read but never emitted, which left it out of DevTools and out of the CSS variables table, and meant a value set anywhere above the list reached the separators by inheritance. It is a real token now — and a token declared on.breadcrumbbeats one inherited from an ancestor, so<nav style="--cui-breadcrumb-divider: '>'">stops working. Move it onto the<ol class="breadcrumb">.- A custom divider now survives in RTL.
--cui-breadcrumb-divider-flippeddefaults to the main divider instead of to a second copy of the Sass default, so setting only--cui-breadcrumb-dividerno longer leaves right-to-left pages on the built-in/. Set the flipped one only for a separator that points somewhere —>becomes<— as before. - New:
.breadcrumb-link, an opt-in class that gives a step a padded, hoverable target. Bare links keep rendering as they always did. On a<button>it looks identical to a link, which is what lets one step toggle a menu of collapsed items — the composition is documented under Collapsed items, together with the reason not to reach forrole="menu"there. - New:
.breadcrumb-divider, a separator you can put in the markup. The automatic::beforedivider stays exactly as it was and existing markup is untouched, but a<li class="breadcrumb-divider">placed between two items carries whatever you put in it — an inline SVG, an icon, a character — with no URL escaping and no variable to set, and an empty one renders the default divider. The automatic divider steps aside wherever an element is placed (.breadcrumb-item + .breadcrumb-itemstops matching), so the two never double up, and the spacing is identical either way. Give the elementaria-hidden="true", since a screen reader would otherwise count the separators as steps in the trail.
Avatar
-
The avatar carries its own colours, and
.theme-{color}tints it.--cui-avatar-bgand--cui-avatar-colorare new: a bare avatar is a neutral circle instead of a transparent one, and a.theme-*class puts the theme colour behind the letters and the colour that reads on it in front of them. The docs used to reach forbg-{color}plus a hand-pickedtext-*, which fails WCAG AA on five of the six examples they shipped — measured 1.85 onbg-warning text-white, 2.94 on info, 3.50 on success, 3.69 on danger, and 3.12 onbg-secondary, which carried no text colour at all. Every theme colour now lands between 4.56 and 18.03. Background utilities keep working on an avatar; they just no longer have to. -
New:
.avatar-subtle, a tinted avatar that keeps the theme colour in the letters rather than behind them. -
Breaking One size token replaces the width/height pair.
--cui-avatar-widthand--cui-avatar-heightalways carried the same value, in the base and in all four size classes, so setting a size meant writing it twice. They become--cui-avatar-size, and--cui-avatar-status-width/-heightbecome--cui-avatar-status-sizethe same way.The label is derived from it —
calc(var(--cui-avatar-size) * .4)— rather than listed per size. That ratio is what every size already used, so nothing renders differently;--cui-avatar-font-sizestill overrides it where a size needs its own.The Sass variables follow:
$avatar-width/$avatar-height/$avatar-font-size/$avatar-status-width/$avatar-status-heightbecome$avatar-size/$avatar-status-sizeplus$avatar-font-size-ratio, and$avatar-sizeskeys drop tosizeand an optionalstatus-size. The$avatar-widthsmap, deprecated since 5.1.0, is gone. -
Breaking
.avatar-imgfills the avatar and crops instead of stretching. It washeight: auto, which deformed every image that was not square; it is nowheight: 100%withobject-fit: cover. Square images look the same. -
Breaking The status indicator’s ring follows the theme. It was
1px solid $white, a hardcoded white that read as a mistake in dark mode. It now comes from--cui-avatar-status-border-color, defaulting to the body background. -
Breaking A stacked avatar lifts on hover instead of pushing its neighbours. The hover changed
margin-inline-end, which moved every avatar after it; it now usestransform, so nothing else shifts.$avatar-transitionchanges frommargin .15stotransform .15s ease-in-outaccordingly. -
Stacked avatars wear a ring and stop pulling what follows them. Each avatar in a stack gets a border in the page background colour (
--cui-avatar-stack-ring-width/-color), so overlapping photos stay separated; the last avatar drops its negative margin, which used to drag the content after the stack underneath it. The overlap is a token now —--cui-avatar-stack-spacing, the fraction of the avatar size — and.avatar-stack-xsthrough-xlsize every avatar in a stack at once. -
The status indicator follows
.theme-{color}and is never invisible..avatar-statusreads the theme slot and falls back to the new--cui-avatar-status-bg(a neutral grey), so a bare indicator reads as “offline” instead of rendering as an empty ring. Background utilities keep working on it.
Alert
-
Breaking An alert without a theme class carries its own surface.
--cui-alert-bgfalls back tovar(--cui-bg-1)instead oftransparent, so a plain.alertno longer takes the color of whatever it sits on. Set--cui-alert-bg: transparenton the alert to restore the old rendering. -
Breaking The alert lays its children out in a row. It is
display: flexwithalign-items: startand--cui-alert-gapbetween children, so an icon next to a label needs nod-flexand no spacing utility. Text mixed with inline elements is a single anonymous item and is unaffected, but an alert built from several block elements — a heading, paragraphs, an<hr>— now sits side by side. Wrap that content in a.vstack:<div class="alert alert-success" role="alert"> <div class="vstack"> <h4 class="alert-heading">Great job!</h4> <p>…</p> </div> </div> -
Breaking A solid alert is
.alert-solid, not a pile of utilities. The docs used to say to drop the contextual class and reach forbg-{color}plus atext-*class picked by hand — and the examples hard-codedtext-white, which fails WCAG AA on five of the eight theme colours: measured 3.50 on success, 3.69 on danger, 2.94 on info, 1.85 on warning and 1.10 on light, where white text on a near-white background is invisible..alert-solidreads the theme’s own contrast token, so every colour lands between 4.56 and 18.03 — including a theme colour you define yourself. Markup using the old recipe still renders; it is the contrast that was never right. -
An alert marked
.fade showfades in when it is added to the page. The classes used to animate the alert in one direction only, because an element inserted with.showalready on it has no earlier state to transition from;@starting-stylesupplies one. Nothing to change in your code — an alert you append at runtime animates without the class juggling that used to be needed, and an alert already in the markup is unaffected. -
The close button is a flex item, not an absolutely positioned one. It is pushed to the end with
margin-inline-start: auto, so it no longer overlaps the text and the alert no longer reserves three paddings of room for it. A.btn-closethat is a direct child of the alert still lands where it did, with or without.alert-dismissible, and.alert-dismissibleitself is now an empty class kept for existing markup. One case does change: a close button nested inside a wrapper used to be positioned by the alert, and is now laid out by that wrapper like any other content. Move it up to be a direct child of the alert to keep it at the trailing edge. -
An
<hr>inside an alert takes the alert’s border colour. The alert redeclares--cui-hr-border-colorfrom the theme’s border colour, falling back to--cui-alert-border-color, so a separator in a.theme-successalert is green rather than the page’s neutral grey, and one in an alert whose border you recoloured follows it. A variant such as.alert-solidpaints its border with the surface colour, so there the separator takes the theme’s border colour, or the text colour without one. -
Breaking
--cui-alert-borderis gone. The border is written in the rule from--cui-alert-border-widthand the border colour, so that a.theme-*class can colour it. Set--cui-alert-border-widthand--cui-alert-border-colorinstead, andborder-styleon the alert for a dashed or dotted line. -
Breaking
$alert-dismissible-padding-ris gone; the space for the close button follows--cui-alert-padding-x. The variable was multiplied out in Sass when the stylesheet was built, so raising--cui-alert-padding-xwidened three sides of a dismissible alert and left the fourth on the default: at2remthe alert had 32px of padding and 48px of room for the button, which also kept its own 16px padding. Both are subtracted from the tokens now —calc(var(--cui-alert-padding-x) * 3)andcalc(var(--cui-alert-padding-y) * 1.25) var(--cui-alert-padding-x)— so one token resizes the whole alert. Defaults render as before; drop any override of the removed variable.
The close button is masked, not filtered
Breaking.btn-close used to paint a black glyph baked into its SVG and recolour it for
dark mode with a filter. The colour now comes from the element, the way the
check, radio, carousel and toggler icons already work:
- background: transparent var(--cui-btn-close-bg) center / 1em auto no-repeat;
- filter: var(--cui-btn-close-filter);
+ background-color: currentcolor;
+ mask-image: var(--cui-btn-close-icon);--cui-btn-close-filteris removed, together with$btn-close-filterand$btn-close-filter-dark. Recolour the button with--cui-btn-close-colorinstead of inverting the image.$btn-close-colordefaults toinherit, so the glyph follows the surface it sits on. A close button inside a themed alert or a dark toast no longer needs a rule of its own to stay legible.- Breaking
.btn-close-whiteis gone, deprecated since v5.0.0, and so is the dark-mode rule that forced the glyph white. Both restated what the inherited colour already produces on a surface that paints its own text — a.text-bg-inversetoast, a dark navbar’s offcanvas, a.bg-body.text-bodysection underdata-coreui-theme="dark". Delete the class; the button renders identically without it. The rule additionally got the mirror case wrong: adata-coreui-theme="light"section inside a dark page inherited the forced white and drew a white icon on white. - Breaking
--cui-btn-close-widthand-heightbecome--cui-btn-close-icon-size. The glyph is square, so one variable sizes it; the padding keeps both axes (--cui-btn-close-padding-y,-padding-x). Nothing changes on screen: the glyph is still1eminside.25emof padding, so the pointer target stays 24px at the default font size — the WCAG 2.2 minimum. - Breaking
--cui-btn-close-bgis--cui-btn-close-icon($btn-close-bg→$btn-close-icon). It stopped being a background when the glyph moved to a mask; the name had been describing the old implementation. - The SVG in
--cui-btn-close-iconcarries the shape only. A custom image works the same way — give it any colour, the mask ignores it.
Badge
-
Breaking Colour comes from
.theme-*, and.badge-*now names the variant. A bare.badgeused to be white text with no background of its own, so it was invisible until a.text-bg-*utility supplied both. It renders neutral on its own now —--cui-badge-colorisinheritand--cui-badge-bgis--cui-bg-2— and a.theme-*class colours it:<span class="badge theme-danger">. The.badge-*namespace carries the three variants instead: solid (default),.badge-subtleand.badge-outline, each mapping the same theme roles..text-bg-*still works and still sets both properties, so existing markup keeps rendering. Markup that paired.badgewith a background utility alone (<span class="badge bg-danger">) now shows the inherited text colour on that background instead of white — swap it for.theme-dangeror add a text colour utility. -
The badge lays itself out.
displayisinline-flexwith--cui-badge-gapbetween children and--cui-badge-min-heightas the floor, so an icon next to a label needs no spacing utility and badges keep one height across a table or a list. -
The font size has a legibility floor.
--cui-badge-font-sizeisclamp(12px, .75em, .75em), so a badge inside small text stops shrinking at 12px while still scaling up with a larger parent. -
New:
.badge-position-{top|bottom}-{start|end}. One class pins a badge to a corner of its positioned parent, centred on the corner, and flips with the writing direction. It replaces theposition-absolute top-0 start-100 translate-middleutility stack the docs used to recommend; that stack still works. The.btn .badgenudge (position: relative; top: -1px) is written as.btn :where(.badge)so a position class on a badge inside a button wins; a stylesheet that relied on beating.btn .badgewith two classes now beats it with one. -
New:
--cui-badge-border-width/--cui-badge-border-color(transparent),.badge-lg, and a link reset. The border tokens are what.badge-outlineswitches,.badge-lgjoins.badge-smon the size scale, anda.badgeno longer inherits the underline from the reboot.
Carousel slides by scrolling
- Breaking
The engine is native CSS scroll snapping, not transforms. The track
(
.carousel-inner) is a scroll container: dragging, swiping and momentum come from the browser, and programmatic jumps animate the scroll position over a fixed 300ms. The transform choreography is gone with its state classes —.carousel-item-next,.carousel-item-prev,.carousel-item-start,.carousel-item-endand.pointer-eventno longer exist; react to theslide/slidevents instead of styling them. The.slideclass is inert — leave it in old markup or drop it, nothing reads it any more. The markup skeleton (indicators + inner with items + controls) is unchanged, and the controls, indicators and captions keep their overlaid layout. - Breaking
rideisautoplay,wrapisends, andtouchis gone.data-coreui-autoplay="true"(boolean — the"carousel"spelling is gone) cycles from page load;endstakes"loop"(default — continues seamlessly where the layout allows),"wrap"(the old jump) or"stop"(hard ends with the matching control disabled). Touch needs no option: the browser owns it. Autoplay now turns off for good once the user interacts (WCAG 2.2.2), and while it runs the active indicator fills like a progress bar over the slide’s interval. - New layout knobs and a pause control.
--cui-carousel-items,--cui-carousel-items-gapand--cui-carousel-items-peekput several slides in the viewport;.carousel-centersnaps the active slide to the middle and.carousel-autolets slides keep their own width. A button with.carousel-control-play-pause(holding.carousel-control-pause-iconand.carousel-control-play-icon) toggles autoplay and swaps itsaria-labelbetweendata-coreui-pause-labelanddata-coreui-play-label. - Breaking
.carousel-dark, thecarousel-dark()mixin and$carousel-dark-*are gone. The chrome colours arelight-dark()pairs resolved on the carousel itself, so the standarddata-coreui-theme="dark"attribute is the dark variant — anddata-coreui-theme="light"pins the white chrome on a dark page the same way. - Every chrome measurement is a
--cui-carousel-*token declared on.carousel— control width and opacities, icon images and sizes, indicator geometry and transitions, caption metrics, the play/pause button — so the whole carousel restyles at runtime without recompiling. - Crossfade timing moved to tokens.
$carousel-transition-durationand$carousel-transitionare gone; the fade reads--cui-carousel-fade-durationand--cui-carousel-fade-timing, and the slides stack on a grid instead of floating.
Autocomplete and Multi Select share one options menu
-
Breaking The generated options dropdown now uses unified
combobox-*classes in both components:combobox-popup,combobox-options,combobox-option(+-with-checkbox),combobox-optgroup(-label),combobox-options-empty,combobox-header,combobox-all, with the state classesselectedandindeterminate. The formerautocomplete-dropdown/autocomplete-option(s)/autocomplete-optgroup*andform-multi-select-dropdown*/-options*/-option*/-optgroup*/-all*/form-multi-selected/form-multi-select-indeterminateclasses are gone, along with their Sass variables and CSS custom properties — theme the shared surface through$combobox-*/--cui-combobox-*instead. This markup is generated by the plugins (authors don’t write it), so only custom CSS targeting the dropdown internals needs updating. The field/selection markup of both components is unchanged. -
Breaking The options list is now a List box instance, so the
combobox-*option classes are gone..combobox-popupstays as the panel; everything inside it is List box markup:v6 alpha now .combobox-header.list-box-header.combobox-all,.combobox-all-with-checkbox.list-box-select-all.combobox-options.list-box-options.combobox-option,.combobox-option-with-checkbox.list-box-option.combobox-option-indicator.list-box-option-indicator.combobox-optgroup.list-box-section.combobox-optgroup-label,.combobox-optgroup-label-with-checkbox.list-box-section-label.combobox-options-empty.list-box-emptydata-valueon an optiondata-coreui-valueThe same move takes the tokens:
--cui-combobox-header-*,--cui-combobox-select-all-*,--cui-combobox-options-*,--cui-combobox-option-*and--cui-combobox-optgroup-*are replaced by--cui-list-box-header-*,--cui-list-box-padding-*/-gap,--cui-list-box-option-*and--cui-list-box-section-label-*. Only--cui-combobox-popup-min-widthis left on.combobox-popup. Option padding follows the List box (.5rem .75rem, indicator in its own grid column) rather than the wider1.25remthe combobox surface used. -
Breaking Multi Select’s
selectAllStyleandoptionsGroupsStyleare removed. A list has one marker, andindicatordecides it for the options, the selectable group labels and the select all button alike. ReplaceselectAllStyle: 'text'/optionsGroupsStyle: 'text'withindicator: 'none'when you want the whole list without checkboxes. -
Breaking Autocomplete keeps the focus in its input while the options list is open. Options take no
tabindexand are never focused; the input carriesaria-activedescendant, and the arrows, Enter and Space are handled there, so Home, End and the space bar keep working as text keys. -
Breaking Multi Select opens a dialog instead. The panel is
role="dialog"named by the control, and the focus moves into it: onto the search box whensearchis on, otherwise onto the option the list points at. Options take a rovingtabindexand the arrows move the focus itself, so there is noaria-activedescendantto read any more. Tab cycles through the panel and the search box without leaving them, and Escape closes the panel and hands the focus back to the control. The select all button left the arrow order — it sits in the panel header, reachable with Tab, and carriesaria-pressed(true/false/mixed); selectable group labels are in the arrow order in its place. -
Breaking Multi Select’s
searchtakestrue,falseor'external', andsearch: 'global'is gone. Typing reaches the search box from anywhere in the component by default, which is what'global'used to turn on, sodata-coreui-search="global"becomesdata-coreui-search="true". SetglobalSearch: falseto keep typing local: the search box then filters only while it has the focus, and a keystroke over the options list jumps to the first option starting with it. Any other string throws aTypeError. -
Breaking Multi Select’s
valuewins over options markedselected. It used to add to them, in the markup and inoptions, and later options applied the start value again; now a givenvalueis the selection, also for options that arrive later for the first time, as in the React and Vue editions, while options shown before keep their state. An array of numbers matches the options, andvalue: ''reads like an emptydata-coreui-value. -
Multi Select takes
search: 'external'. The search box then filters nothing,search.coreui.multi-selectcarries thequery, andsetOptions()replaces the options without rebuilding the control or touching the selection; a selected value the new options lack keeps its chip and is still sent with the form, unless it is disabled.setLoading()shows aloadingLabelrow below the options meanwhile, and they stay usable.resetSelectionOnOptionsChangeempties the selection when the new options carry different values, for options that depend on another field. -
Multi Select’s
search.coreui.multi-selectfollows the text, not the keys. It fires when the text of the search box changes, a paste included, and no longer on a key that leaves the text alone, such as an arrow, or when the panel opens or closes over an empty box. -
Multi Select’s
dispose()gives the native select back the options it had before the component started. A select built fromoptionsin the config, or one whose optionssetOptions()replaced, used to keep the generated options; an empty<select>givenoptionsis empty again afterdispose(). -
Breaking Multi Select sends one
change.coreui.multi-selectper change of the selection. The clear button,deselectAll(),selectAll()and a pick that replaces the value of a single select used to send one event for every option they touched; they send one now, once the native select holds the new value and the control shows it, the way the nativechangeworks. An action that changes nothing sends no event, native ones included, and the component no longer sends one for each preselected option while it builds or rebuilds. A selection the page gives sends none, fromsetValue()or from a givenvaluethat options fromsetOptions()bring later, and a native reset of the form that changes the selection sends one, as Autocomplete, Chip Input and Combobox do. -
Breaking Form controls fire their change event once per change of the value, on a form reset as well, and never from a value the page gives. OTP Input’s
change.coreui.otp-input, Rating’sreset(), Autocomplete’sclear()and Range’schanged.coreui.rangeused to fire even when the value stayed; they fire only when it changes now, and Range no longer fires while it starts or on thechangeafter aninput. OTP Input and Range Slider now fire on a native form reset that changes the value, and Date Input sends the nativeinputandchangeof its value field there too.setValue()of OTP Input does not fire. A form reset brings Rating and Range Slider back to the value they were created with, not one a latersetValue()gave, and OTP Input’sreset()puts back the code it started with, as a native reset does. A stepper keeps the validation marks of a form reset a listener cancels. -
Multi Select keeps its selection through
setOptions(); it used to fall back to the options markedselected. A selected option that is disabled, also inside a disabled group, shows as a disabled chip and stays through the clear button,deselectAll()and Backspace; the options of a disabled group are disabled in the list and in the native select as well, so, like any disabled option, they are not sent with the form. A single select whose selection is empty holds itsvalue=""placeholder option, or none, instead of letting the browser fall back to the first option, so the form gets no option the control does not show. -
Breaking Autocomplete’s
searchtakes only'external', and global typing isglobalSearch, on by default. The array and the comma list are gone, sosearch: ['external', 'global']anddata-coreui-search="external, global"becomesearch: 'external', andsearch: 'global'goes away: typing on the picker or clear button, or in the panel, reaches the field without it. SetglobalSearch: falseto keep a keystroke where the focus is. An array now fails the option type check, sonew coreui.Autocomplete(el, { search: ['external'] })throws until it is updated, and a string other than'external'no longer turns external search on, so a page still writing the comma list in a data attribute filters the server’s results again. The space bar and shortcuts with Ctrl or Cmd stay with the button that has the focus. -
Autocomplete no longer gives the host
tabindex="-1"and keeps atabindexthe page wrote out of the tab order while it lives, so the field is the only tab stop; a click on the frame puts the focus in the field rather than only opening the panel. Withsearch: 'external'the panel opens for the options you set even when they do not contain the typed text. -
Breaking Autocomplete’s typeahead hint no longer reaches the server. With
showHintson, the hint input behind the field carried anameand, being readonly rather than disabled, was serialized with the form: the configurednamewhen there was one,<uid>-hintwhen there was not. A form therefore submitted either the field twice with an empty string in front of the value, or an extra…-hintkey nobody asked for. The hint now carries noname, sonew FormData(form).getAll(name)returns one entry andform.elements[name]is an input rather than aRadioNodeList. Code written around the old shape, such as readinggetAll(name)[1], needs the plain value. -
Breaking Multi Select’s search field no longer reaches the server. The input that filters the options carried a
nameand is a plain text input, so every form holding a Multi Select submitted whatever the user had typed to narrow the list:search-<name>when the select had a name,search-<uid>when it did not. It carries nonamenow, thesearch-…key is gone from the payload, and the input setsautocomplete="off"so the browser stops offering to fill it. -
Breaking An unnamed Multi Select stays out of the form. A
<select>the page had not named was givenname="<uid>", so its value was submitted under a key nobody chose. The attribute is written only when there is a name, from the option or from the element.dispose()also takes back theidand thenamethe component added, where it used to leave both behind. -
Breaking An unnamed Autocomplete stays out of the form too. Its input took a generated
namewhen the option was not set, so a form carried a key nobody chose. It does not now. The generatedidis unaffected. -
Breaking Validation state comes from
validationState.invalidandvalidare deprecated aliases ofvalidationState: 'invalid'and'valid', andvalidationStatealso takes a state from$form-validation-states. A given'invalid', from the options or from an.is-invalidclass in the markup, now blocks the submit, and the next change the user makes to the value drops it, while a change from code keeps it. The markup class is read once, when the component starts, so it no longer comes back when the text returns to the value it started with, a native form reset drops it, and a class written later is not read. A validation that reports the input, from a submit or the page’s owncheckValidity(), now shows on the frame, and the autocomplete is the only one that writesaria-invalidand the message link on its input: the form plugin and the stepper leave the input to it. See Validation.
Menu (new) and Dropdown
-
New component: Menu — contextual overlays with submenus, container teleport, responsive placement and logical (
start/end) placements, ported from Bootstrap v6 with CoreUI’s class conventions. See the Menu docs. -
New component: Context Menu (PRO) — a right-click, long-press or Shift+F10 inside an element with
data-coreui-toggle="context-menu"opens a.menuat the pointer and suppresses the browser’s own menu. Built on Menu, so items, submenus, keyboard navigation and dismissal are the same;show({ x, y })opens it at a point from JavaScript. -
Dropdown is now a compatibility layer over Menu. The v5 markup, classes and
*.coreui.dropdownevents keep working unchanged — the plugin shares Menu’s engine underneath. Where their options overlap, Dropdown also understands Menu’s additions (for examplecontainer). Deprecation notice: Dropdown may be removed in v7 — prefer Menu for new work. -
Breaking The responsive alignment classes
.dropdown-menu{-sm|-md|-lg|-xl|-2xl}{-start|-end}moved tocoreui-legacy.css, and.dropup/.dropend/.dropstartno longer carry the menu’s static position. Alignment that changes with the viewport is a responsive placement on the toggle —data-coreui-placement="bottom-start lg:bottom-end"— which now also works withdata-coreui-display="static"and inside a navbar, because the plugin writes the resolved placement onto the menu asdata-coreui-placementand the stylesheet positions the static menu from that attribute rather than from the wrapper class.dropdown.cssloses every@media; the direction classes keep the caret and still decide the placement when no option is given..dropdown-menu-endand.dropdown-menu-startstay.- <ul class="dropdown-menu dropdown-menu-lg-end"> + <button data-coreui-toggle="dropdown" data-coreui-placement="bottom-start lg:bottom-end"> + <ul class="dropdown-menu">
List Box (new)
- New component: List Box (PRO) — a keyboard-driven
list of values on the ARIA listbox pattern, with
none/single/multipleselection, sections, descriptions, links, an empty state and anactiveDescendantmode that leaves the focus in your own input.
Combobox (new)
- New component: Combobox — a
.form-controltoggle next to a popup List box, where the options are markup you write, with single and multiple selection, an optional search, a checkbox indicator and a hidden select that takes part in form validation and submission.
Transfer (new)
- New component: Transfer (PRO) — two list boxes and
a pair of buttons that move the selected options between them, with a
select-all header and counter per list, an optional search, a one-way mode and
an announcement of every move through
announce().
Form (new)
- New plugin: Form (PRO) — validates a form with
data-coreui-validateandnovalidateon submit: stops an invalid submit, marks the invalid controls with.is-invalidandaria-invalid, links their messages witharia-describedbyand moves focus to the first. It replaces the starter script of the validation page, see Validation.
Modal and Offcanvas on the native <dialog>
- Breaking
Both components render on the native
<dialog>element now, opened withshowModal()/show()and closed withclose(). The browser provides the top layer (noz-indexmanagement), the::backdrop, focus trapping, background inertness and the dialog ARIA semantics — the markup dropsrole,aria-modal,aria-hiddenandtabindex="-1". - Breaking
The modal wrapper layers are gone.
.modal > .modal-dialog > .modal-contentflattens to a single<dialog class="modal">holding.modal-header/.modal-body/.modal-footerdirectly. Size (.modal-sm/-lg/-xl) and fullscreen (.modal-fullscreen*) modifiers sit on the dialog itself..modal-dialog-scrollableis renamed to.modal-scrollable;.modal-dialog-centeredis gone — the browser centers a modal dialog by default. Offcanvas markup only swaps the tag:<dialog class="offcanvas offcanvas-start">. - Legacy v5 markup keeps working — for now. When a component is
instantiated on v5 markup (via data API or JavaScript), the plugin rebuilds
it into the
<dialog>structure at runtime and logs a console warning. The old element stays in the DOM as an inertdisplay: contentsshell around the new dialog, so existing element references and event listeners keep working (our events bubble through it). Selectors and custom CSS targeting.modal-dialog/.modal-contentare the one thing that cannot be preserved. This transitional layer will be removed in v7. - Breaking
.modal-backdropand.offcanvas-backdropare gone — the backdrop is the native::backdroppseudo-element. Style it through the new--cui-modal-backdrop-bg/--cui-modal-backdrop-opacityand--cui-offcanvas-backdrop-*tokens.$zindex-modal-backdropand$zindex-offcanvas-backdropare removed. - Breaking
Animation opt-in is inverted. Modals animate by default;
.fadeis gone and.modal-instant/.offcanvas-instantdisable animation instead. The auto-upgrade maps a missing.fadeto.modal-instant. The offcanvas.showingstate class is replaced by the native[open]attribute (.hidingremains for the exit transition). - Breaking
The Modal
focusoption is removed — the native dialog always traps focus.handleUpdate()remains as a no-op for API compatibility; the scrollbar-compensation logic it triggered no longer exists (scroll lock isoverflow: hidden+scrollbar-gutter: stableon the root element, with thedialog-openclass). - New capabilities: a
modal: falseoption opens the dialog non-modally (page stays interactive); acancel.coreui.modalevent fires when a modal is dismissed with Escape; toggling from a trigger inside an open modal swaps to the target modal with a seamless backdrop (no flash); modals can be stacked — the top layer manages paint order. - Sass:
$modal-transitionand$modal-show-transformare replaced by$modal-transition-duration/$modal-transition-timing; the offcanvas transition token splits the same way (--cui-offcanvas-transition-duration/-timing).
Dialog (new) and Drawer (new)
-
One backdrop model for both.
:rootemits--cui-backdrop-color,--cui-backdrop-opacityand--cui-backdrop-blur; the dialog’s and the drawer’s::backdropmix colour and opacity from them, so either can be retuned alone. The looks are unchanged (dialog: black at 50%/65%, drawer: the page colour at 25%), and.backdrop-scrim,.backdrop-translucentand.backdrop-noneswap them from the markup..dialog-no-backdropand.drawer-no-backdropkeep working. The sidebar’s backdrop tokens are--cui-sidebar-backdrop-zindex/-bg/-opacitynow (were unprefixed--cui-backdrop-*, which the new root tokens would have shadowed). -
New components: Dialog and Drawer — the native-
<dialog>popup and side panel with the Bootstrap 6 look anddialog-*/drawer-*classes: Dialog is a centered floating panel over a blurred backdrop with optional slide entry variants; Drawer is a floating, inset, rounded panel with translucent/sheet/fullscreen/fit-content variants and responsive classes (.drawer-sm….drawer-2xl). Both share the DialogBase behavior with Modal/Offcanvas, which keep the classic look and the v5 vocabulary as compatibility layers over it — deprecated in v6 and removable in v7, so reach for Dialog and Drawer in new work. See the Dialog docs and Drawer docs.
Translucent surfaces share two tokens
.navbar-translucent, .toast-translucent, .card-translucent,
.drawer-translucent and .menu-translucent used to carry their own mix
amount and filter — two as Sass variables, three as literals — at 85% or 80%
depending on the component. All five now read --cui-translucent-opacity
(80%) and --cui-translucent-backdrop-filter (blur(5px) saturate(180%))
from :root, so one override retunes every translucent surface and one on an
element retunes that surface alone. Consequences: the navbar, the toast and
the card become slightly more see-through (85% → 80%; set
--cui-translucent-opacity: 85% on them to keep the old look), and
$navbar-translucent-bg-opacity, $navbar-translucent-backdrop-filter,
$toast-translucent-bg-opacity, $toast-translucent-backdrop-filter,
$card-translucent-bg-opacity and $card-translucent-backdrop-filter are
removed in favour of $translucent-opacity and
$translucent-backdrop-filter. The card’s caps keep their own amount as the
--cui-card-translucent-cap-bg-opacity token on .card-translucent.
Calendar
- Changed. The range band sits under the day cells and follows the
theme.
--cui-calendar-cell-range-bgis--cui-primary-bg-subtle(the theme’sbg-subtleunder a.theme-*class) instead of a 12.5% tint laid over the cells, so the selected start and end keep their solid fill and the day numbers are no longer tinted. A hovered day in the range takes--cui-primary-bg-muted(the theme’sbg-muted), one step stronger than the band. - Changed.
minDateandmaxDateare compared by the day. v5 compared a day cell at midnight with the instant, so a limit that carried a time of day, such asminDate: new Date(), disabled its own day. Now that day can be picked; the Date Picker’s time selects and the field bound the time on it. - Changed. A week row stands for the ISO week that holds most of its
days. With
firstDayOfWeekother than1a row spans two ISO weeks, and v5 numbered it by its last day but picked the week of its first day: a Sunday-first row from August 30 to September 5, 2026 showed 36 and picked2026W35, and that value given back asstartDatedid not select the same row. The number, the picked value, the weeks ofstartDate,endDate,minDateandmaxDate, and the range and its preview now all follow the row’s majority week, so that row is2026W36and the picked date is its Monday, August 31, the date2026W36stands for. With Tuesday to Thursday first, the numbers move instead, so the row from September 1 to 7 shows 36 where it showed 37. - Changed. In week selection
disabledDatesdisables a week only when it disables every day of it, as it does for a month, quarter or year. Before, a week was disabled when its row’s first day was: with Monday firstdisabledDates: [new Date(2026, 7, 31)]disabled week 36, and with Sunday first a function that disables weekends disabled every row. A range of weeks across a week that holds a disabled day is kept, as a range of months is. - Changed. In week selection
minDateandmaxDatecount as whole weeks, so their weeks can be picked on any first day of the week, as the month, quarter or year of a limit can in those selections, and the months view opens a month only when its page holds a week inside them. Before, aminDatelater than the first day of its row disabled that row, and with Tuesday to Thursday first amaxDateof2026W36disabled the row of week 36. - Breaking
cellHoveris gone. It reported the date under the pointer or the focus for the hover preview in the date picker’s field, which v6 no longer draws. To follow the pointer or the focus over a calendar, listen formouseoverorfocusinon its element: each day, month, quarter and year cell the calendar shows carries its date indata-coreui-date, in thetoDateString()form, and a week row carries it on the day cells it shows. Unlike the event, the listeners also see cells that cannot be picked, and onlymouseoutorfocusouttells that the pointer or the focus left. - Breaking
selectAdjacementDaysandshowAdjacementDaysare spelledselectAdjacentDaysandshowAdjacentDays— the option keys, thedata-coreui-select-adjacent-days/data-coreui-show-adjacent-daysattributes and the picker passthrough alike. The typo is fixed in the React and Vue libraries in the same release. - Breaking
update(config)is gone; the dates change throughsetStartDate(),setEndDate()andsetCalendarDate(), and the other options are fixed once the calendar starts. A call likeupdate({ startDate })used to reset every other option (range,calendars,locale, …). See Live changes go through setters, notupdate(config). - Breaking
Each grid is one tab stop, and
tabindexno longer marks a selectable cell. The arrows,Home,Endand the page keys move within a calendar and Tab leaves it, which is whatrole="grid"promises. Every cell insideminDate/maxDatecan take the focus, also one that cannot be picked: the keys stop on a date indisabledDatesinstead of skipping it, a screen reader announces it as unavailable, and Enter and Space do nothing there, so a month where nothing can be picked is explored like any other. A selectable day, month, quarter, year or week row carriesdata-coreui-selectable, andtabindex="0"sits on one cell per grid, so a two-month range picker keeps a stop in each month that reaches into the limits. Custom CSS or scripts that reached for.calendar-cell[tabindex="0"]need.calendar-cell[data-coreui-selectable]. - Breaking
Day cells write
data-coreui-datein thetoDateString()form ("Thu Sep 17 2026"), like the month, quarter and year cells, instead of the fullDate#toString()value. A script that selected a day cell with[data-coreui-date="${date}"]needs[data-coreui-date="${date.toDateString()}"]. - Breaking
Picking a date no longer fires
calendarDateChangeand keeps the month on screen. A picked day of an adjacent month (selectAdjacentDays) used to become the calendar date while the grid stayed put, so the next-month button then skipped a month; it now stays where it was and shows as selected with an outline on a subtle background instead of the solid fill (--cui-calendar-cell-selected-adjacent-color,--cui-calendar-cell-selected-adjacent-bg,--cui-calendar-cell-selected-adjacent-hover-color,--cui-calendar-cell-selected-adjacent-hover-bg). Listen tostartDateChange/endDateChangefor the picked date. - A selected day or week keeps its colour under the pointer, a shade darker
(
--cui-calendar-cell-selected-hover-bg,--cui-calendar-cell-selected-hover-color); it used to take the hover colour until the pointer left. - In the years view the navigation names the page it shows,
2020 – 2031, instead of the one year the page is built around, so turning a page is announced as the new range. The grid label and the navigation write the range in the locale’s own digits. - In the years view the double arrows and
Page Down/Page Upturn the calendar by the twelve years a page shows instead of ten, so a turn no longer repeats two years of the previous page. - In the years view the year button goes back to the view that picks the date instead of doing nothing, and it is disabled in year selection, where the years view is the one that picks.
- Without
weekNumbersLabelthe week-number column is named for screen readers with the locale’s word for week, in a visually hidden span; before, its header was empty. Each week number is named the same way with its number (Week 36), and the newariaWeekNumberLabeloption takes a function of the week number to name it yourself. - Breaking
The years view names its double arrows with new
ariaNavNextYearsLabel/ariaNavPrevYearsLabeloptions ('Next 12 years'/'Previous 12 years') instead of the year labels, which stay on the days, months and quarters views. A page that translatesariaNavNextYearLabel/ariaNavPrevYearLabelneeds to translate these two as well. - A grid in which nothing can be picked is described by the new
ariaNothingToPickLabeloption ('Nothing on this page can be picked'), also announced when the calendar turns to such a page, so a page that translates the other calendar labels translates this one too. - A pick is announced through the new
ariaSelectedDateAnnouncementandariaSelectedRangeAnnouncementoptions ('Selected date: {date}','Selected range: {start} to {end}'); before, picking a date with Enter left a screen reader silent. A page that translates the other calendar labels translates these two as well. - The toggle of a date picker and of a date range picker is named with the
value the picker holds (
Toggle calendar, Friday, August 14, 2026), so a screen reader reads the picked date when the popup closes and focus goes back to it. A toggle the page named itself keeps its name. - The month and year between the navigation arrows are no longer a live region.
A page turn is announced through
announce(), inside the panel of a date picker when the calendar sits in one, and a key that leaves a day or a week row names no page, since the date that takes focus names it: a key that moved from August 31 to September 1 used to be read twice, as the date and then asSeptember 2026. - A locale whose own calendar has different months (Persian by default in
fa-IR, or an Islamic, Hebrew or Chinese calendar asked for with-u-ca-) now shows the calendar in the Gregorian calendar, in its own language and digits. The grid was always Gregorian, sofa-IRused to show September 1, 2026 as the 10th of Shahrivar 1405 and name the January cell Dey. Calendars that share the Gregorian months, such as Thai Buddhist or Japanese, keep their year numbering in the calendar. The date field lays out and reads Gregorian numbers for every locale, without the locale’s era. - Breaking
A cell drawn by
renderDayCellno longer gets anaria-label, so its content is what a screen reader reads. Every render function receives the cell’s full name asmeta.label(Wednesday, August 12, 2026,August 2026,Q3 2026,2026); put it in the cell, for example in avisually-hiddenspan next to a short visible label markedaria-hidden="true". Day cells the calendar draws itself announce the full date with the weekday instead of the short numeric date (8/12/2026), and in week selection the row that takes focus is named by the days it spans (August 10 – 16, 2026), after its week number when week numbers are shown (Week 33, August 10 – 16, 2026), instead of reading out all seven. - The copy of today among an adjacent month’s days no longer takes the
todayclass,aria-current="date"ormeta.isToday, so a two-month calendar marks one current date and a date picker opens on today only when its own month is shown.renderDayCellreceives the fullmetain week selection too, not onlylabel. - A cell that cannot be picked carries
aria-disabled="true": a day outsideminDate/maxDateor indisabledDates, a week row, month, quarter or year outside the range, and in day selection a day among an adjacent month’s days unlessselectAdjacentDaysis on. Such an adjacent day is no longeraria-selectedeither, andrenderDayCellgetsisDisabled: trueandisSelected: falsefor it, so the copy of a picked date that fills the next panel reads as unavailable filler. - With
selectAdjacentDays, the range and its hover preview are drawn on the adjacent month’s days as well, andrenderDayCellgetsisInRange: truefor them, as for the month’s own days. Before, both stopped at the edge of the month. - With
range, the grid of the view that picks the range isaria-multiselectable, and every day, week row, month, quarter or year of the selected range that can be picked carriesaria-selected="true", not only its two ends, so a screen reader announces the ones in between as selected. The months and years views of a day range only navigate and keep it on the ends, andmeta.isSelectedstill marks only the ends. - In week selection,
renderDayCellgets theisDisabled,isInRangeandisSelectedof the day’s week row, the way the calendar draws it: every day of a selected week isisSelected, every day of a week in the range isisInRange, and a day isisDisabledonly when its row cannot be picked. Before, each field described the day alone, so a selected week reported only the day holding its start or end date as selected. - A year below 100 stays as written: a calendar on December of year 99 draws
the days of year 99, where it drew 1999, and its next panel shows January of
year 100, where it showed January 2000. The month, quarter and year views,
the arrow keys and paging keep the year the same way. Years before 1 cannot
be picked, as dates before
minDatecannot: their cells are disabled, and the arrow keys and paging stop at 1 January of year 1. - A year string written in four digits keeps its year:
0026-05,0026W05,0026Q2,0026and12/31/0099given to the calendar stand for the years 26 and 99, where they became 2026 and 1999. Two digits still expand to the century within 50 years of today. The value reported for week, month, quarter and year selection writes the year in at least four digits (0026-05, where it was26-05), so it reads back as the same year. - Additive:
startDateChangeandendDateChangenow carry adateObjectproperty with the underlyingDate, alongside the existingdateproperty.dateremains formatted perselectionType— aDatefor day selection, but a string for week ("2026W27"), month ("2026-01"), quarter ("2026Q1") and year ("2026"). Existing listeners are unaffected. - Breaking
startDateChangeandendDateChangeno longer report a date that stays empty. Picking a day before the start of a range, or an end across a disabled day, used to fireendDateChangewithnullalthough no end had been picked, and clearing a range across a disabled day before a start was picked firedstartDateChangewithnull. A day picked again is still reported. .monthsand.yearsare gone from the stylesheet. Their one rule hid a.time-pickerthat followed a.calendarpanel in the months or years view among its siblings, where no picker renders one, so it matched nothing. The calendar names the view it shows on the cell content:.calendar-cell-inner.day,.month,.quarterand.year.- The navigation puts the year button before the month button in locales that write the year first (for example Japanese, Chinese, Korean, Hungarian or Lithuanian), the way the locale’s own dates read.
Date Picker / Date Range Picker
BreakingThe pickers are rewritten. DatePicker, DateRangePicker and TimePicker
are now a composition of the section-based input fields
(date input), the calendar and a
shared popup primitive, instead of the v1 monolith. The rewrite ships under
the existing names — DatePicker, data-coreui-date-picker,
.date-picker — so no renaming is needed on your side, but the options below
changed. The v1 shells are gone; if you need to compare against the old
behaviour, v5 is the reference. Mapping:
- Removed — projected regions replace button configuration.
footer,todayButton,todayButtonClasses,cancelButton,cancelButtonClasses,confirmButton,confirmButtonClasses,ranges,rangesButtonsClasses→ put a<template data-coreui-template="footer">/data-coreui-template="ranges"child in the picker element. Zero-argument actions bind withdata-coreui-picker-action="today|clear|reset|close"; actions with arguments (range presets) go throughpicker.getContext()(setDate,setRange,clear,reset,close+ read-only state). - Removed — obsolete with section fields.
inputDateFormat,inputOnChangeDelay,inputReadOnly,placeholder— the masked field is always a valid partial date; there is no free-text parsing to debounce. Section placeholders come from the section fields’ own per-section options —dayPlaceholder,monthPlaceholder,yearPlaceholderand the rest. - Removed —
previewDateOnHoverandseparatoron the date range picker. The fields no longer preview the hovered day in a second, read-only input; the calendar still highlights the range up to the hovered day. The separator between the two fields is always rendered, andseparatorIcon/separatorIconRtlreplace its icon. - Kept.
timepicker: truestill adds the time half to the date picker, now as a row of time selects under the calendar rather than a second panel beside it. Because the two halves own different parts of one value, a picker with time does not auto-close: picking a day keeps the time and picking a time keeps the day.secondsnarrows what the time half offers andselectionOptionsis its escape hatch. - Changed. With
timepicker, aminDateormaxDatethat carries a time of day bounds the time on its day: the time selects disable the hours, minutes and seconds beforeminDateor aftermaxDatethere, a time the panel sets beyond the limit (the current time when that day is picked) moves to the limit, and a typed time beyond it marks the field invalid. A limit without a time of day covers its whole day. - Kept. Calendar passthrough (
locale,minDate/maxDate,disabledDates,selectionType,showWeekNumber, custom cell renderers, …),container,startDate/endDate/date,startName/endName/name,size, validation states, the data API (data-coreui-date-picker), and the imperative API (show/hide/toggle/clear/reset). - Passthrough is by name. Options that belong to the inner calendar or date
input are forwarded automatically, so
data-coreui-selection-type,data-coreui-show-week-numberanddata-coreui-formatwork on the picker element.calendarOptions/inputOptionsremain as the programmatic escape hatch. - Every selection type has a matching mask. Month and year selection narrow
the mask to
MM/yyyyandyyyy. Week selection renders the native<input type="week">presentation — a fixed localized label plus week and year sections (Week 29, 2026,Tydzień 29, 2026), where the year is the ISO week-numbering year. Quarter selection narrows toQQQ yyyy(Q4 2026). The v1 text input rendered2026W27/2026Q1; the submitted value now follows the mask (setformat="yyyy-Www"for an ISO-style field), and the event carries the v1-formatted string inevent.formattedDatenext to theDateinevent.date(see “Picker events can be cancelled” below). The format parser gainedww(ISO week) andq/QQQ(quarter) tokens, plus quoted literals ('Week' ww— fixed text that may contain token letters,''escapes a quote). - Format comes from the mask.
inputDateFormatis replaced byformat(forwarded to the field);inputDateParsestill applies to pasted text. - Footer buttons are projected,
today/nowactions keep the v1 gating. A projected button opting into thetodayaction (nowon the time picker) is disabled at initialization when its target falls outsideminDate/maxDate/disabledDates— matching the v1 built-in button. This is one-way: the picker only ever setsdisabled, never re-enables. The state of any other projected button is yours — the slot context exposesisDateSelectable(date)(isTimeSelectableon the time picker) for computing it. Programmatic dates (setDate, actions, the initialdateconfig) run through the same min/max validation as typed ones: out of range, the field turnsis-invalid,errorChangefires, andgetDate()reportsnull.dateChangeon the shells always carries the validated value, andsetDate()does not fire it. - The calendar is built on first open, not at construction. It accounts for
~83% of a date picker’s DOM (and ~89% of a range picker’s), and it is not
observable before the popup opens, so the shells create it in
show()and seed it from the field values. Measured over 20 pickers: date picker 65 ms → 15 ms and 3200 → 540 DOM nodes, range picker 93 ms → 12 ms and 6060 → 740 nodes. The first open costs a few milliseconds more (the calendar is built then); later opens are unchanged. Code reaching into the privatepicker._calendarbefore the first open now findsnull. setRange()andreset()on the Date Range Picker show the range in the calendar, on the month of its start date (of its end date when there is no start), in whatever view the calendar is in and also when the range does not change, so a preset clicked again after browsing brings its range back. The calendar used to move only when a field changed, and went to the month of the end date when both changed.clear()leaves it where it is;setDate(),today()andreset()on the Date Picker already showed their date.
The pickers take their validation state from validationState
Breaking
invalid and valid on DatePicker, DateRangePicker and TimePicker are
deprecated aliases of validationState: 'invalid' and 'valid', and
validationState also takes a state from $form-validation-states. A given
'invalid' now blocks the submit, and the next change of the value the user
makes (a pick in the panel, the cleaner, a footer or custom-range button that
changes the value, typing in the field) drops it; closing the popup and a
change your code makes keep it. An .is-invalid or .is-valid class on the
picker element is read once, when the picker starts. An aria-invalid on it is
not read at all; remove it, since the date range picker makes its element a
group and the attribute would be announced on that group. Set the state after
start with setValidationState(). See the Validation sections of the
date picker,
date range picker and
time picker.
The date picker adopts the markup you write
The date picker is composed from the date input, the calendar and a popup,
joined by the one date it owns. Mark a div with data-coreui-picker-field,
a button with data-coreui-picker-toggle or data-coreui-picker-cleaner
inside the picker element and it uses them instead of generating its own —
a trigger can be a regular button with text, the icons can be plain <svg>
in the HTML. Nothing changes for the bare <div data-coreui-date-picker>.
Breaking
aria-expanded moved from the picker’s root element to the toggle button,
where it describes the control that opens the calendar; the toggle also
carries aria-controls and aria-haspopup="dialog". Styles keyed to the
root’s .show class are unaffected.
The date range picker is built on the date range input
BreakingThe range picker no longer builds its two fields itself: it composes the new date range input, which owns both dates, their validation and the arrow between them. Two consequences:
-
The picker element is no longer the frame. It wraps one:
<div class="date-picker date-range-picker picker"><div class="form-control-group form-date-range">…. CSS or scripts that reached for.form-control-groupon the picker’s own element now find it one level down;.show,.date-range-pickerand thedata-coreui-toggleattribute stay where they were.aria-expandedmoved from the element to the toggle button, which also carriesaria-controlsandaria-haspopup="dialog". -
The two array options are split in two, following the rule that every label is its own option:
ariaLabels: ['Start date', 'End date']becomesariaStartLabel/ariaEndLabel, andfloatingLabels: ['Check-in', 'Check-out']becomesstartFloatingLabel/endFloatingLabel.- data-coreui-floating-labels='["Check-in", "Check-out"]' + data-coreui-start-floating-label="Check-in" data-coreui-end-floating-label="Check-out"
Picker events can be cancelled, and dateChange has one shape
-
Breaking
dateChange/startDateChange/endDateChangeon the Date Picker and Date Range Picker carry the same payload whichever way the date arrived.event.dateis the validatedDate(ornull) the field holds, andevent.formattedDateis that value in the shape ofselectionType. A selection from the calendar used to put the formatted value inevent.dateand theDateinevent.dateObject, while a date typed into the field put theDateinevent.date— the same listener saw two shapes.- const date = event.dateObject ?? event.date + const date = event.date -
Breaking The pickers emit their change events only when the value changes. A pick of the value already selected, and
clear()orreset()when the picker already holds that value, no longer firedateChange,startDateChange,endDateChangeortimeChange; v5 fired them on every pick and on everyclear()andreset(). The newsetDate(),setTime(),setStartDate(),setEndDate()andsetRange()fire no change event at all. -
shownandhiddenon the pickers wait for the popup’s transition, like the modal, dialog, toast and collapse events:shownfires once the panel has finished opening (and not at all when it closes first),hiddenonce it has finished closing and left the page (and not at all when it reopens first). v5 fired both as soon asshow()orhide()ran. -
showandhidecan be cancelled on the Autocomplete, Multi Select, Date Picker, Date Range Picker and Time Picker, the way they already could on Modal and Dialog:event.preventDefault()in ashowlistener keeps the panel closed, in ahidelistener keeps it open, and the matchingshown/hiddendoes not fire.hideon a panel that is not shown no longer emits anything.
Date Input / Time Input
- The
disabledoption is the only switch. A.disabledclass written on the field does not disable it: the field sets the class from the option. reset()restores the date the field was created with, not the last one passed toupdate(), and emitsdateChangewhen the value moves. A native form reset (form.reset(), a reset button) now resets the field too.- RTL keyboard navigation now follows the visual direction in nested RTL
contexts. Arrow keys (and
Home/End) mirror when the field renders right-to-left; the direction is read from the element’s computed style instead ofdocument.documentElement.dir, so adir="rtl"ancestor works and not only a document-level direction. - Breaking
A required field blocks the form. The value moved from an
<input type="hidden">, which the browser leaves out of validation, to a<textarea>laid invisibly over the field, withtabindex="-1"andaria-hidden="true"; a textarea, unlike a text input, leaves Enter submitting a form that has no submit button. Arequiredfield that is empty, incomplete or holds a rejected date now stops the submit, and the browser puts focus on its first empty section with its own message over the field. The field marks itself invalid whenever a validation reports it, also outside a submit, and its value field firesinputandchangewhen the date changes. This applies to the date and time pickers and to both ends of a date range as well. Code that selectsinput[type="hidden"]inside the field needs.form-date-time > textarea.
Time Picker
-
Breaking The
rollinfix is gone from the class and token names. It marked which of two renderings a body used, and the time picker has one now, so.time-picker-roll-coland.time-picker-roll-cellare.time-picker-coland.time-picker-cell, with--cui-time-picker-col-*and--cui-time-picker-cell-*in place of their-roll-spellings..time-picker-rollitself was always present on.time-picker-bodyand its declarations moved there, so the modifier is gone rather than renamed;--cui-time-picker-body-paddingwent with it, since the modifier zeroed the padding it set. -
secondsgoverns the field as well as the panel, and both are on by default, as they were in v5. v5 formatted its value with seconds, so10:30:00 AMis what both majors submit.seconds="false"takes the panel column and the field’s seconds section away together; with the section present the value is complete only once the seconds are typed, the same as every other section of the field. -
Breaking
hoursis 0 to 23 in every locale. v5 read the list and the filter in the column’s cycle, so on a locale whose v5 columns were 12-hour (the ones that write AM and PM in Latin letters, such asen-US)hours: [1, 3, 5, 7]meant 1, 3, 5 and 7 in the morning and in the afternoon,12meant noon and midnight, and a filter received 1 to 12. The 12-hour column still lists 1 to 12 next to AM/PM, but each entry now stands for the 24-hour hours the list allows: picking an hour that only one period allows sets that period, and the period the picked hour does not allow is disabled. To keep such a list, name both halves:[1, 3, 5, 7, 13, 15, 17, 19], with0and12for12. More locales get 12-hour columns than in v5, because the columns follow the field’s clock, Korean, Traditional Chinese, Arabic and Mexican Spanish among them; their v5 columns were 24-hour, so a list written for them keeps its meaning. -
The day period column speaks the locale. It read “AM” and “PM” in every locale; it now shows the names the field shows, 오전 and 오후 in Korean. A longer name widens its column, so
--cui-time-picker-cell-widthis now the narrowest a cell gets, and--cui-time-picker-cell-padding-xkeeps the name off the cell’s edges. -
The time selection body is now a shared primitive used by both pickers, so the
hours/minutes/secondsfilters and the 12/24-hour locale split behave identically in each. See time picker and the date picker’s own time half. -
The time row sits under the calendars again, with its clock icon. v6.0.0-alpha put it beside them in a column of its own;
.date-picker-bodyis a grid with the v5 areas once more, so.date-picker-timepickersspans the calendars it belongs to, with its clock icon back as an inline SVG that takes its colour from--cui-date-picker-time-icon-color. -
Breaking The date picker’s time controls carry its own name.
.time-picker-inline-iconand.time-picker-inline-selectare.date-picker-time-iconand.date-picker-time-select, with--cui-date-picker-time-icon-*and--cui-date-picker-time-select-*in place of the--cui-time-picker-inline-*tokens. They sit in the date picker’s panel and are styled from its stylesheet, so nothing about them belonged to the time picker but the name. The time picker’s own panel is untouched. -
The time selection is one tab stop, and the arrows move between its parts. v6.0.0-alpha gave a tab stop to every column of the roll, so Tab walked the hour, minute, second and AM/PM lists one by one. The body is a composite now, like the section field it mirrors: Tab reaches it once, Arrow Left and Arrow Right move between the parts and stop at the ends, and Arrow Up / Arrow Down still change the focused value. The date picker’s time selects answer the same keys.
-
Each picker has one time rendering, and the
variantoption is gone. The time picker shows the roll; the date picker’s time half shows the selects, which is what v5 rendered inside a date picker with time. Each picker builds its own rendering, so the choice is no longer an option anywhere;selectionOptionsstill reaches the body a picker built, for the hour and minute filters and the labels. The date picker’s time selects moved with it: it now ships in the date picker stylesheet, so a page that only uses the time picker no longer downloads it. -
Same removals as the date pickers:
footer/cancelButton/confirmButtonand their class options give way to a projected<template data-coreui-template="footer">withdata-coreui-picker-action="now|clear|reset|close";inputReadOnly,inputOnChangeDelayandplaceholderare obsolete with a section field. -
reset()restores the value the picker was constructed with. In v2 the shell remembers it: setting a value goes through the field’supdate(), which rewrites the field’s own configuration, so the field can no longer restore the original on its own.
The v1 picker shell classes are gone
BreakingThe rewritten pickers style the field through
forms/_form-date-time.scss and render their icons as inline SVG, so the v1
shell’s own elements no longer exist: .date-picker-input,
.date-picker-input-wrapper, .date-picker-input-preview,
.date-picker-cleaner, .time-picker-input and .time-picker-cleaner.
Custom CSS targeting those needs rewriting against
the new structure; everything else (.date-picker, .date-picker-popup,
.date-picker-body, .date-picker-footer, the -sm/-lg size modifiers, the
.time-picker-* bodies) keeps its name.
The picker panel is .date-picker-popup / .time-picker-popup
Breaking
-dropdown was the vocabulary of the per-family panels, and every other family
lost it when the options menu became .combobox-popup. The pickers were the
last holdout:
| v5 | v6 |
|---|---|
.date-picker-dropdown | .date-picker-popup |
.time-picker-dropdown | .time-picker-popup |
The rename comes with the reason for it. The panel declares its own tokens
now rather than inheriting them from the field’s root through a
*:not(.date-picker) > … patch, so nothing inside it depends on having a
particular ancestor — it renders the same in place, teleported through the
container option, or rendered into document.body by a framework port’s
portal.
width: min-content went with it: an absolutely positioned panel already
shrinks to its content, and both pickers measure identically without the
declaration.
.form-select is absorbed by .form-control
-
A
<select>takes.form-controlnow, like every other control. The two were already the same thing everywhere but the chevron: measured across every computed property,.form-selectand.form-controlon the same<select>differed in exactly ten — the eightbackground-*that draw the chevron and the padding that makes room for it. Everything else already came from the shared--cui-control-*tokens. -
<select class="form-control">used to render without any arrow at all —.form-controlsetsappearance: none, which removes the native one, and nothing put ours back. That is fixed by the same change. -
New:
.form-control-caretputs the chevron on an element that is not a<select>— a button that opens a listbox, for instance. -
.form-select,.form-select-smand.form-select-lgkeep working as aliases of the.form-controlclasses, not as a second implementation. Deprecated in v6, removed in v7.- <select class="form-select form-select-lg"> + <select class="form-control form-control-lg"> -
Breaking The
$form-select-*Sass variables are gone, along withscss/forms/_form-select.scss. Most were pass-throughs to$input-*/--cui-btn-input-*and their.form-controlequivalents apply unchanged; the chevron’s own are renamed$form-control-select-*. The runtime tokens move with them:--cui-form-select-bg-img→--cui-control-select-bg-img,-bg-position,-bg-size,-indicator-padding→--cui-control-select-padding-end, and the-feedback-icon-*pair. -
Breaking The chevron is translucent, and its color is no longer a Sass variable.
$form-control-select-indicator-colorand its dark counterpart are gone; the SVG carries#00000080and#ffffff80, and the control declares both images as--cui-control-select-bg-imgand--cui-control-select-bg-img-dark, dark mode switching which one thebackground-imagereads. A color inside a data URI cannot read a token, so making it translucent is what lets the chevron sit correctly on a field of any color instead of on the two we picked. Measured on the default field, the stroke goes fromrgb(44, 49, 60)torgb(127, 127, 127)in light mode and fromrgb(223, 223, 223)torgb(132, 133, 135)in dark. Override the token with your own data URI to get a solid one back. -
Two differences a select inherits from
.form-control, neither of which changed a rendered box in any case measured. It paints its background withbackground-clip: padding-box, the value.form-controlhas always used to avoid a rounded-corner artefact in Firefox — 0.12% of the control’s pixels, the antialiasing of the four corners. And.form-select-sm/-lgnow carry themin-heighttheir.form-controlcounterparts always had, so a sized select is finally the same height as a sized input; it only shows if you have cut the padding below that floor.
Form labels carry their own weight
- Breaking
.form-labeland.col-form-labelrender medium and full-contrast by default.$form-label-font-weightisvar(--cui-font-weight-medium)(wasnull— labels inherited the surrounding weight) and$form-label-colorisvar(--cui-fg-body)(wasnull), so a label inside a muted or tinted context keeps the page’s text colour instead of fading with it. Set the knobs back tonull, or--cui-label-font-weight/--cui-label-colortoinherit, for the old behaviour.
Validation: data-coreui-validate replaces .was-validated
Breaking
Client-side validation no longer uses .was-validated or the bare :valid /
:invalid pseudo-classes. Add data-coreui-validate to the <form> instead:
the styles ride the :user-invalid pseudo-class, so feedback appears only
after the user interacts with a control or attempts to submit — no JavaScript
toggles a class anymore, and after an AJAX submission there is nothing to
remove to reset the form’s appearance.
- Success styling is opt-in.
data-coreui-validatealone shows only invalid styling; setdata-coreui-validate="valid"to also colour the valid fields. - Server-side validation is unchanged —
.is-invalid/.is-validkeep working globally, without the attribute. - Feedback of a check, a radio, a switch, a
.form-controlor.form-selectin an input group, or a control placed directly in a component frame belongs to the.form-fieldaround it. The field shows it while the control carries the state, wherever it sits in the field, for example in a.form-field-contentnext to the label, and a check, a radio or a switch colours the label. Feedback right after any other control still follows it.
- <form class="needs-validation" novalidate>
+ <form data-coreui-validate novalidate>The starter JavaScript is gone. A form with data-coreui-validate and
novalidate is handled by the new form plugin: it
stops an invalid submit, marks the invalid controls with .is-invalid and
aria-invalid="true", links the message of each to it with aria-describedby
while it stays invalid, and moves focus to the first. Remove the copied
script; to show the errors your own way, call preventDefault() on
validate.coreui.form. Components follow the attribute:
the stepper marks the controls of a novalidate step form itself each time
it validates the step, passing or not: .is-invalid on the invalid ones (and
.is-valid on the valid ones under data-coreui-validate="valid"), and the date / time / date-time inputs key their submit-time state off the
attribute instead of the class.
The stepper also sets aria-invalid on those controls and moves focus to the
first of them, and its panes are no longer aria-live: while a control is
invalid, the stepper adds its .invalid-feedback to aria-describedby instead,
the one the stylesheet shows for it (after it, or in the .form-field around
the controls the field page lists) or the one named in
data-coreui-invalid-feedback, so the message is read with the field. The panes used to read out every message that appeared in them;
a message that changes while the user types is now silent, so pass the ones
that have to be heard to announce().
Going back to an earlier step no longer validates the current one.
stepValidationComplete.coreui.stepper now fires before the stepper decides,
as validate.coreui.form does, so a validity its handler sets with
setCustomValidity() blocks the step, and event.isValid tells whether every
control passed before the handler ran. next() and finish() fire it once
per step instead of twice.
Form components share one validation state
Breaking The multi select, the chip input, the combobox, the OTP input, the date and time fields, the pickers and the autocomplete take and show their validation state the same way, described under Form components:
- The state comes from the
validationStateoption, or from.is-invalidor.is-validon the component’s element, read once when the component starts. A class or anaria-invalidwritten on the element later is not read; set the state withsetValidationState(). - A given
'invalid'blocks the submit, with the text of the component’s invalid feedback as the browser’s message, until the user changes the value or the form is reset, which removes the state. - The form plugin and the stepper leave the field that carries a component’s value to the component, which shows the state they report on its frame.
- After taking the result over with
preventDefault()onvalidate.coreui.form, keepevent.isValidrather than callingcheckValidity(): the call reports every invalid field again, and the components then show their error.
Stepper: next() on the last step finishes the stepper
next() on the last enabled step now does what finish() does: it validates
the step and, when it passes, finishes the stepper and fires
finish.coreui.stepper. It used to validate the step and stop, so a wizard
driven by a single Next control never finished. A step button with
.disabled does not count, so next() finishes on the step before it.
Stepper: finish() before the last step does nothing
finish() called on an earlier step used to move to the next step, the same
as next(). It now does nothing until the last enabled step is active, the
same as in React and Vue. Call next() to move forward.
Stepper: the active indicator’s background follows the theme
Breaking--cui-stepper-step-indicator-active-bg is removed. The active step’s
indicator background is a 5% tint mixed on the step from the theme colour, or
from --cui-stepper-step-indicator-active-color without one, so a .theme-*
class on the stepper or on a single step button tints it along with the text
and border. To give the active indicator another background, set
--cui-stepper-step-indicator-bg on .stepper-step-button.active.
Input group: .has-validation removed
Breaking
.has-validation is gone. It existed so a group could keep its corner logic
with a -feedback element sitting inside it as the last child — in v6 the
feedback lives outside the group: wrap the group in a
.form-field and put the -feedback after it, and the
message renders under the whole frame. A group still carrying the class simply
gets the default corner handling; a group still carrying the feedback inside
should move it out.
- <div class="input-group has-validation">
- <span class="input-group-text">@</span>
- <input type="text" class="form-control is-invalid" required>
- <div class="invalid-feedback">Please choose a username.</div>
- </div>
+ <div class="form-field">
+ <div class="input-group">
+ <span class="input-group-text">@</span>
+ <input type="text" class="form-control is-invalid" required>
+ </div>
+ <div class="invalid-feedback">Please choose a username.</div>
+ </div>Input group: one frame class instead of five
Breaking.input-group squared off the frame of a field component by naming each
family’s wrapper — .autocomplete-input-group, .date-picker-input-group,
.time-picker-input-group, .form-multi-select-input-group and
.form-password. All five became .form-control-group, so the input group
targets that one class:
- > .autocomplete:not(:last-child) > .autocomplete-input-group,
- > .date-picker:not(:last-child) > .date-picker-input-group,
- > .time-picker:not(:last-child) > .time-picker-input-group,
- > .form-multi-select:not(:last-child) > .form-multi-select-input-group,
- > .form-password:not(:last-child) > .form-control,
+ > .form-control-group:not(:last-child),A field component is the frame — it carries .form-control-group on its own
root rather than nesting one — so it is a direct child of the input group like
any other control, and there is no wrapper level to reach through.
If you wrote CSS against those wrapper classes to adjust a field inside an
input group, target .form-control-group instead. A hand-authored group — a
password field, a chip input — is now a first-class child of .input-group
and needs no wrapper of its own.
The input group styles ship in the components layer, not forms. It
assembles controls rather than being one, and the layer it sat in lost to
components whatever the selector said — so a .btn inside a group kept the
radius it sets for itself and showed rounded corners against the field. If your
own stylesheet re-squared those corners as a workaround, the override is no
longer needed. A rule of yours that relied on the group losing to a component
now loses to the group instead; give it a layer of its own if you need it back.
Chip Input keeps focus among the chips after a removal
Removing the focused chip used to send focus back to the text field. It now moves to the next chip, else the previous one, and only reaches the text field once no chip is left, the same rule Chip Set follows, so several chips can be removed in a row from the keyboard.
Chip Input is submitted only once you name it
BreakingThe hidden input that carries the chips took a generated name when the option
was not set, so a form carried a key nobody chose. It is named only when you
ask for one, and the option table no longer lists the name as auto-generated.
dispose() also removes that input, where it used to leave it in the page.
Chip Input carries its chips in a <textarea>
Breaking
The chips travel in a hidden <textarea> instead of an <input type="hidden">,
because the browser leaves a hidden input out of validation. The submitted value
is the same, the chips separated by commas, and the id and name options land
on the <textarea>. A script or a selector that looked for
.chip-input input[type="hidden"] looks for .chip-input > textarea instead.
With it the chip input takes required and validationState, blocks the submit
while it is empty and required, and a native form reset puts back the chips it
started with. A disabled chip input is no longer submitted, like any disabled
field. See Validation.
Chip Input takes the frame class itself
Breaking- <div class="form-control-group chip-input" data-coreui-chip-input>
+ <div class="chip-input" data-coreui-chip-input>A chip input has no control to wrap — the element is the frame — so it adds
.form-control-group to itself rather than asking you to write both. Writing
both still works.
Chip Input reads the group’s tokens
.chip-input no longer keeps a private copy of the frame. --cui-chip-input-min-height
and --cui-chip-input-gap are gone: the element reads --cui-control-min-height
and --cui-control-group-gap from .form-control-group like every other group,
and .chip-input-sm/-lg switch those tokens instead of private ones. The Sass
variables that only re-pointed a --cui-btn-input-* token ($chip-input-min-height,
-font-size, -bg, -color, -border-width, -transition-* and the size
variants of min-height and font-size) are removed; what remains is what the chip
input sets differently — padding, border colour and radius, and the gap, per
size. The disabled chip’s opacity reads --cui-control-group-disabled-opacity
instead of a literal .65. Rendered values are unchanged.
Number Input (new)
- New component: Number Input (PRO) — a number field
with a stepper you can actually hit. The browser’s own spinner is tiny, shows
only on hover in some engines and does not exist at all on iOS; the component
replaces it with two buttons sized like every other adornment, on the shared
.form-control-groupframe. Nothing to migrate — it is listed here because thenumber-inputname, its classes and its Sass variables are new in v6.
The disabled option is the only switch of the form components
Combobox, Autocomplete, Multi Select, Chip Input, Chip Set, Chip, Rating, Date
Input and Time Input take their disabled state only from the disabled option
(data-coreui-disabled="true"). A .disabled class in the markup, or a
disabled attribute on the select a multi select enhances or on a combobox
toggle button, does not disable them. All but Chip Set set the class from the
option, Combobox and Multi Select also that attribute; Chip Set passes the
option to its chips. dispose() leaves the class and the attribute for the
next instance to write again from its options. In v5 a .disabled class disabled a chip, a chip set and
a chip input, and the autocomplete data API skipped an element with the class;
write data-coreui-disabled="true" instead, on a single chip too. A
disabled class returned by chipClassName no longer disables a chip.
A disabled multi select now disables its select, so the form leaves it out of
validation and of the submitted data, as it does for the other disabled
fields; a required multi select that is disabled no longer blocks the
submit.
Multi Select
- The option checkboxes are the standard form-check surface. The indicators
used to re-implement the form-check visuals as
::beforepseudo-elements with their own variable set; they are decorative.form-check-inputspans now (aria-hidden, sincearia-selectedcarries the semantics), so size, border, radius, background and theming all come from the form-check surface and the two stay in lockstep. The per-indicator look variables are gone with them — only$combobox-option-indicator-widthsurvives, and it follows the checkbox:--cui-check-sizewhere the indicator can see it, and the shared--cui-control-sizeotherwise. Theme the option checkbox through$check-*/--cui-check-*instead. - Validation state comes from
validationState, and the select is no longer watched.invalidandvalidare deprecated aliases ofvalidationState: 'invalid'and'valid', andvalidationStatealso takes a state from$form-validation-states. A given'invalid'now blocks the submit, and the next change the user makes to the selection drops it. An.is-invalidor.is-validclass on the select is read once, when the component starts, and moves to the frame; a class oraria-invalidwritten on the select later is not read, so set the state withsetValidationState(). A native form reset restores the selection the control started with. See Validation. - Backspace in the empty search box focuses the last chip. It
removed the last value at once; the first press now moves the focus to the
last chip and the second removes it, as in Chip Input. The arrow keys move
between the search box and the chips, the selection is one tab stop, removing
a focused chip moves the focus to its neighbour, and a value removed through a
chip, Backspace or the cleaner is announced (
ariaRemovedAnnouncement). A single select and thecounterandtextselections still remove the last value at once. selectionType: 'chips'is the default, and'tags'is deprecated. The two render the same chips;'tags'keeps working until v7.ariaTagDeleteLabelis deprecated in favour ofariaChipRemoveLabel; the old option still names the remove button and takes precedence while it is set.
Password Input and Number Input build their own frame
BreakingBoth components supply their own buttons, so they supply the frame that lays them out. The markup is a plain form control carrying the toggle:
- <div class="form-control-group">
- <input type="password" class="form-control">
- <button type="button" class="form-control-action" data-coreui-toggle="password" aria-label="Toggle password visibility">
- <span class="form-password-action-icon"></span>
- </button>
- </div>
+ <input type="password" class="form-control" data-coreui-password-input>data-coreui-togglemoves from the button to the input, and its value is the component’s name:password-input(andnumber-inputfor the new Number Input). The button no longer exists in your markup, so there is nothing left to put it on.- Classes you write on the input, other than
.form-control, move onto the frame — size, spacing, width. They describe the field, and once the input is wrapped the field is the frame; a margin left on the control would sit inside the border. They move back ondispose(). - An input already inside a
.form-control-groupis left where you put it, so a field can share a frame with other adornments, and only a frame the component created is removed ondispose(). - The accessible name of the toggle is the
ariaToggleLabeloption now, since the button is no longer yours to label.
Password Input: the toggle icon comes from JavaScript
BreakingThe toggle button carried <span class="form-password-action-icon"></span>,
and CSS painted it as a mask that swapped through a sibling selector. The
plugin renders it now — inline SVG on currentColor, like every other
adornment in the library — so the button needs no icon markup at all:
<div class="form-control-group">
<input type="password" class="form-control">
- <button type="button" class="form-control-action" data-coreui-toggle="password" aria-label="Toggle password visibility">
- <span class="form-password-action-icon"></span>
- </button>
+ <button type="button" class="form-control-action" data-coreui-toggle="password" aria-label="Toggle password visibility"></button>
</div>.form-password-action-iconis gone, andscss/forms/_form-password.scsswith it. That takes 2.5 kB of URL-encoded SVG out of the stylesheet — it was there for every page, whether or not one had a password field.$form-password-icon-show/$form-password-icon-hideand the matching--cui-form-password-icon-*properties are gone. Supply your own artwork through the newshowIcon/hideIconoptions instead, the way the other adornments already take theirs. Both are sanitized;allowList,sanitizeandsanitizeFncontrol that.- The plugin now initializes on
DOMContentLoadedfor markup carryingdata-coreui-toggle="password", so the icon is present without any script of your own — the same data-API pass Chip, Chip Set, Chip Input and Search Button already make.
Password Input: the toggle is disabled with the field
The toggle button used to stay enabled on a disabled field and ignore the
click, while toggle() flipped a disabled field anyway. The button is now
disabled with the field and leaves the tab order, like the other action
buttons, and neither the button nor toggle() changes what a disabled field
shows: a password revealed before the field is disabled stays visible, and
can be hidden once the field is enabled again.
Icons rendered by JavaScript
- New components render their icons as inline SVG on
currentColor(the chips pattern) instead of CSSmaskdata-URIs: swap them with theindicatorIcon/separatorIconoptions (sanitized with the SVG allowlist), recolor them with plain CSScolor. The corresponding$date-picker-*-icon/-icon-sizeSass variables disappear with the v1 shells. - The calendar navigation icons moved to JavaScript too. The four
$calendar-nav-icon-{prev,next,double-prev,double-next}Sass variables are removed, along with the CSS custom properties and mask rules that painted them. Customize them with the newnavPrevIcon/navNextIcon/navPrevDoubleIcon/navNextDoubleIconcalendar options instead (inline SVG, sanitized).$calendar-nav-icon-width/-height/-color/-hover-colorstay — the icon inheritscolor, so recoloring is plain CSS. Calendar’s defaultallowListis nowSVGAllowlist(wasDefaultAllowlist), so the component can render its own inline SVG icons — and custom cell renderers may return SVG as well. Strictly more permissive; scripts and event handlers are still stripped.- Directional icons are resolved in JavaScript, not CSS. v1 swapped the
range separator with a second Sass variable
(
$date-picker-separator-icon-rtl) applied through an RTL mask; v2 picks betweenseparatorIconandseparatorIconRtlwhen it renders, based on the element’s computed direction — so adir="rtl"ancestor works, not just a document-level direction. The calendar mirrors its prev/next icons the same way. Consequence for custom code: a component that resolves directional icons at construction time must be in its final DOM position when it is created — a detached element has no computed direction (the nearest ancestor that declaresdiranswers for it,dir="auto"included, and the document after that, but placement first is the reliable path).
Date and time fields are submitted only once you name them
BreakingDate Input and Time Input took an id written on the host as
permission to submit, naming their hidden input <component>-<id> when no
name was configured, so <div data-coreui-date-input id="delivery"> posted
date-input-delivery — a key the page never asked for, from an attribute a
<label for> needs anyway. Date Picker and Time Picker
inherit the rule through the field they embed, and with floatingLabel that
field is given a generated id, so they posted a key that changed on every load
(date-input-date-picker-190423). The value field is named from name alone
now, and a field without one stays out of the form. dispose() removes the
sections, the separators and the value field, takes back the aria-label it
generated, and gives the host the author’s own child nodes, the same nodes,
not copies. It used to leave a live-looking
field behind that kept submitting.
Range / Range Slider
-
Changed. A pressed thumb takes
--cui-primary-bg-muted, the theme’sbg-mutedunder a.theme-*class, instead of a 30% tint of the primary colour ($form-range-thumb-active-bg). -
Breaking A Range Slider handle with no name of its own is no longer submitted. An array of names shorter than the handle count gave the leftover handles the literal name
undefined, so a form carried anundefined=key. Those handles are unnamed now, as they already were in the React and Vue editions. -
The filled track, value tooltip and tick marks come from a
Rangeplugin. Wrap the input in<div class="form-range">and give the input.form-range-input; the plugin writes--cui-range-fill(0–1) on the wrapper and the CSS draws the fill from it,data-coreui-tooltipsadds a value tooltip on hover, drag and focus, or permanently with"always"(the sametooltips/tooltipsFormatoptions as the range slider), and a linked<datalist>renders tick marks laid out on a grid. Every such wrapper initializes itself. The input fireschanged.coreui.rangewith the numericvalue. The fill is the theme colour at half strength, a shade lighter than the thumb. The bare<input class="form-range">keeps working unchanged as the CSS-only form of the control; it gets no plugin, no fill and no instance. New tokens on.form-range:--cui-range-track-fill-bg,--cui-range-track-disabled-bgand the--cui-range-tick-*family (labelled ticks use-height, unlabelled ones-minor-height). -
The range track sits halfway between
--cui-bg-2and--cui-bg-3(color-mix(in oklch, …), was--cui-bg-3), in both the native range and the range slider.--cui-bg-3and thesecondarycolour resolve to the same grey in light mode, so atheme-secondarythumb and fill vanished against the track. -
Validation reaches the range.
.is-invalid/.is-validon the input (either form) colour the thumb and its focus ring with the state, and a feedback element after the wrapper toggles through:has(). -
Breaking The range slider renders the markup of the range. It builds
<div class="form-range">inside its element, with one native.form-range-inputper handle, so it looks like a range and reads its tokens; the slider keeps the handles in order, moves them and draws the band itself. The class names follow:.range-slider→ the inner.form-range,.range-slider-input→.form-range-input,.range-slider-vertical→.form-range-vertical,.range-slider-tooltip→.form-range-tooltip(now a<div>),.range-slider-ticks/.range-slider-tick→.form-range-ticks/.form-range-tickwith the text in.form-range-tick-label, and the per-tick.clickable→.form-range-ticks-clickableon the container..range-slider-inputs-containerand.range-slider-trackare gone (the first input draws the track), and so is the.disabledclass on the element (the inputs are disabled); the stylesheet styles none of the.range-slider-*classes any more. The handles no longer carryrole="slider",aria-valuemin,aria-valuemaxoraria-valuenow: the native inputs expose them. Options, events and methods keep their names, apart fromariaLabels(below); thechanged.coreui.rangeevent that an inner Range fired on each handle is gone, so listen toinput.coreui.range-sliderandchange.coreui.range-slideron the slider. The range tokens are declared on that inner.form-range, so a--cui-range-*token set on the slider element itself (inline, or by a class on it) no longer reaches the handles: target its.form-range(.my-slider .form-range { … }). -
Breaking The range slider’s
ariaLabelsoption isariaLabel. It takes an array with one name per handle, as before, a comma-separated string, or a function(index, total) => string, the shape the OTP input uses for its digits;data-coreui-aria-labelsbecomesdata-coreui-aria-label. -
The range slider element is a group named after the field. The component gives it
role="group", so anaria-labeloraria-labelledbyon it names the group, and a screen reader announces it when the focus enters the slider. With several handles, each handle carries only its own name (“Minimum value”); a single handle takes the name of the group, followed by itsariaLabelwhen it has one.aria-describedbymoves onto every handle. Arolewritten on the element is replaced withgroup. -
Breaking The range slider takes a validation state. The new
validationStateoption, or.is-invalid/.is-validon the slider element read once when it starts, shows on the element and on the handles, and a given'invalid'blocks the submit through the first handle until the user moves a handle or the form is reset. A class written on the element later is not read; set the state withsetValidationState(). See Validation. -
Range slider interaction follows the native control. Pressing a handle no longer moves it a step; a press on the track or a clickable tick moves the nearest handle and it follows the pointer (mouse, touch or pen), with
change.coreui.range-slideron release instead of on press; the events carry numbers; a form reset restores the configured values instead of the middle of the range;setValue()anddispose()touch only what the slider built, so content of the element stays. -
Breaking The range slider’s handles and track read the range’s tokens. The
--cui-range-slider-thumb-*and--cui-range-slider-track-*families are gone; the slider’s.form-rangedeclares the--cui-range-*family instead, so one setting styles both controls. Rename:--cui-range-slider-thumb-{width, height,bg,border,border-radius,box-shadow,active-bg,disabled-bg}→--cui-range-thumb-*,--cui-range-slider-thumb-transition→--cui-range-thumb-transition-{property,duration,timing},--cui-range-slider-track-{width,height,cursor,bg,border-radius,box-shadow}→--cui-range-track-*,--cui-range-slider-track-in-range-bg→--cui-range-track-fill-bg,--cui-range-slider-disabled-track-in-range-bg→--cui-range-track-disabled-bg. The Sass variables follow ($range-slider-thumb-*,$range-slider-track-*→$form-range-*).--cui-range-slider-label-*,--cui-range-slider-tooltip-margin-*and--cui-range-slider-vertical-*are gone too, with their Sass variables ($range-slider-label-*,$range-slider-tooltip-margin-*,$range-slider-vertical-track-*): tick labels read--cui-range-tick-label-colorand-font-size, the tooltips sit like the range’s, a vertical slider’s height is--cui-range-vertical-heightand its track width--cui-range-track-height; the tick label padding has no counterpart. The filled band keeps its half-strength mix of the theme colour, now shared with the range’s fill, and a disabled slider greys the band through the theme slot on the track. -
Breaking The range slider’s
labelsoption isticks. Every entry draws a tick mark, with or without text, so the option and everything around it carry the name theRangeplugin uses:labels→ticks,clickableLabels→clickableTicks(data-coreui-ticks,data-coreui-clickable-ticks),.range-slider-labels-container→.form-range-ticks,.range-slider-label→.form-range-tick, and--cui-range-slider-label-*→--cui-range-tick-label-*. The entry shape is unchanged: a string, a bare number, or{ value, label, class, style }. -
Range slider ticks sit on a grid. The plugin builds
grid-template-columns(rows when vertical) from the gaps between tick values, so ticks are in normal flow: the container sizes itself, and the resize listener plus the measured container height are gone. Every tick draws its mark from--cui-range-tick-*— taller when it has text — and the ticks are sorted by value. A bare number inticksis a tick without text at that value. The newlistoption reads a<datalist>like the range does and merges its options withticks. Custom CSS that positioned.range-slider-tickthroughleft/bottomno longer applies. -
Range slider tooltips follow the range’s modes.
tooltips: trueshows a handle’s tooltip on hover, drag and visible focus;"always"keeps them on. -
A theme beats
--cui-range-thumb-bg, as everywhere else. The token carried the--cui-theme-*slot inside its own default, so setting it inside a themed container dropped the theme. The slot sits on the property now. -
Breaking The value bubble is a tooltip. The plugin renders
<div class="form-range-tooltip tooltip bs-tooltip-top">(vertical:bs-tooltip-start, orbs-tooltip-endin a right-to-left page) with.tooltip-arrowand.tooltip-inner, so the bubble looks like every other tooltip on the page — dark on a light page — and follows--cui-tooltip-*and the theme slots. The twelve--cui-range-slider-tooltip-*look tokens (-bg,-color,-padding-*,-font-size,-border-radius,-box-shadow,-arrow-*,-max-width,-transition,-zindex) and their Sass variables are gone, and so are-margin-bottomand-margin-end. The tooltip declares its own--cui-tooltip-*tokens, so set them on it: for the previous light bubble, give the slidertooltipClass: 'light-tip'and declare--cui-tooltip-bg: var(--cui-bg-4)and--cui-tooltip-color: var(--cui-fg-body)on.light-tip. -
Breaking The pressed thumb adapts to the color mode. Its color was 70% white mixed into the theme color at build time, so in dark mode the pressed thumb flashed a near-white pastel. It now mixes toward
--cui-body-bgat runtime — identical in light mode, dark and correct in dark mode. -
Both sliders are themeable: the filled track and the thumbs read the theme slots, so a
.theme-*class recolors them; without one they stay on primary as before.
Sidebar
- Every
--cui-sidebar-*token comes from a Sass map ($sidebar-tokens,$sidebar-nav-tokens,$sidebar-narrow-tokensand the variant maps), so the sidebar takes@use ... with ()overrides like every other component. The narrow variant’s variables ($sidebar-narrow-*) live insidebar/_sidebar-narrow.scss. - Hover and active links read the foreground ladder. Their text was the
pure black/white
--cui-emphasis-colorand their background the Sass-computed--cui-tertiary-bg-translucent; they are--cui-fg-bodyandcolor-mix(in srgb, var(--cui-fg-body) 10%, transparent)now — a tint that works on any surface and can be retuned in the browser. Slightly stronger than before on a light sidebar, slightly weaker on a dark one. - Breaking
--cui-tertiary-bg-translucentis gone from:root. The sidebar was its only reader. Set--cui-sidebar-nav-link-hover-bg/-active-bgon.sidebar-navfor a different tint. - Theme variants.
.sidebar.theme-{color}paints the surface, text, icons and the hover/active links from that colour’s slots..sidebar-lightjoins.sidebar-darkas the scheme switch. - Breaking
.sidebar-bodyand.sidebar-formare removed. Neither carried styles beyond the sidebar’s own padding..sidebar-self-hiding-*had already gone in v6 — a sidebar that hides itself below a breakpoint ishide()from a resize handler, or the mobile mode the sidebar takes belowlgon its own. - The mobile backdrop reads the shared backdrop tokens.
.sidebar-backdropmixes--cui-backdrop-colorat--cui-backdrop-opacityand blurs by--cui-backdrop-blur, like the dialog and the drawer;--cui-sidebar-backdrop-bgcarries the mix and--cui-sidebar-backdrop-blurthe blur, and--cui-sidebar-backdrop-opacity(.5, the old fade target) is gone. The.backdrop-*helpers work on the sidebar too. .sidebar-narrowgets its bottom padding back. It read--cui-sidebar-toggler-heightfrom a token declared on the toggler, a descendant, so the padding silently fell to 0; the token is declared on.sidebarnow.
ScrollSpy
- Breaking
The active link is chosen by an activation line, not by scroll
direction. The plugin used to compare each section’s offset with the
previous scroll position, so the same scroll offset could highlight a
different link depending on which way you arrived. It now activates the
lowest section whose top edge has crossed a line drawn
topMarginbelow the top of the scroll container — a pure function of the scroll position. Two visible consequences: the last active link stays highlighted while a gap between sections crosses the line (the navigation no longer goes blank), and a short last section is activated when the container is scrolled to the bottom, even though its top never reaches the line. - New option
topMargin(default12%) places the activation line; it takes a percentage of the container’s height or a pixel value (96px). A pixel line is recomputed on resize. - Breaking
rootMargindefaults tonulland is now the raw override. Set it and it is handed to theIntersectionObserverunchanged, taking precedence overtopMargin; a v5 configuration such asrootMargin: '0px 0px -25%'keeps working as written.thresholddefaults to[0]instead of[0.1, 0.5, 1]— with an activation line a section only needs to report whether it crosses it. smoothScrollfinishes the navigation. Once the scroll settles, the URL hash is replaced with the link’s target and focus moves to the section.- Breaking
The deprecated
offsetoption is removed — it has been a thin alias since the IntersectionObserver rewrite. UsetopMargininstead:offset: 100→topMargin: '100px'.
Tab
- Breaking
Constructing a Tab outside a tab-panel parent now throws a
TypeErrorinstead of silently returning a half-initialized instance, matching upstream. The silent variant hid real markup mistakes: the instance existed but never activated anything. - The
data-coreui-toggle="pill"and"list"toggles stay — they are documented markup, and v6 keeps documented markup working. $nav-link-transitionsplits into$nav-link-transition-property/-duration/-timing, emitted as--cui-nav-link-transition-*. The link’s transition was compiled into its rule while.tab-panein the same file already read the three-token form, so half the file could be retimed in the browser and half could not. Setting--cui-nav-link-transition-durationnow takes effect; before it was ignored.- Breaking
.nav-linkis a flex row, and it fills on hover and when active. The link lays its children out with--cui-nav-link-gap,--cui-nav-link-alignand--cui-nav-link-justify, so an icon and its label line up without a wrapper. It also takes--cui-nav-link-hover-bgand--cui-nav-link-active-bg, rounded by--cui-nav-link-border-radius— the base nav used to change text color alone. Set the two backgrounds totransparentto keep the old look. Under a theme, hover reads--cui-theme-fgand the lighter--cui-theme-bg-subtle, and the active item the darker--cui-theme-bg-muted, so hovering a link never outweighs the selected one..nav-tabs,.nav-underlineand.nav-underline-bordersquare the link off, and the two underline variants take neither fill, hover or active: they mark their state with a line, and a radius curves that line’s ends while a fill says the same thing twice. - Breaking
.nav-tabsdraws its line with an insetbox-shadow, not a border. The links no longer pull themselves over the container’s border with a negativemargin-bottom; each link carries its own bottom border (the line colour at rest, the panel colour when active) and the container paints the line asbox-shadow: inset 0 calc(-1 * var(--cui-nav-tabs-border-width)) 0 var(--cui-nav-tabs-border-color). The rendered result is pixel-identical, but custom CSS targeting.nav-tabs’border-bottomor the links’ negative margin needs a second look; recolour the line through--cui-nav-tabs-border-color..card-header-tabscompensates with a deeper negative margin instead of zeroing the border. --cui-nav-gapis new, declared on.navand defaulting to0, so the base nav renders exactly as it did. It gives the token a home on the component whose name it carries:.navbar-navalready overrode it, and.nav-underlinekeeps its own--cui-nav-underline-gap.
Tooltip
--cui-tooltip-opacitynow blends into the background colour instead of fading the whole element. The old element-levelopacity: .9also faded the label text, compositing it below the WCAG 1.4.3 contrast minimum (about 3.5∶1). The surface and arrow keep the same translucent look; the text renders at full opacity. The token still works — override it and only the background alpha changes.- Breaking
$tooltip-marginand--cui-tooltip-marginare removed. The variable had beennulland deprecated since v4; the token compiled to an empty value and themargindeclaration reading it never took effect. Position a tooltip with the JSoffsetoption instead. - Roomier tip, less see-through.
--cui-tooltip-padding-yiscalc(var(--cui-spacer) * .375)(was.25rem),--cui-tooltip-padding-xiscalc(var(--cui-spacer) * .75)(was.5rem) and--cui-tooltip-opacityis.95(was.9). Set the three tokens on.tooltipto keep the old proportions. - Tooltips take theme classes. The surface and the text read the
--cui-theme-bg/--cui-theme-contrastslots, with--cui-tooltip-bgand--cui-tooltip-coloras the fallbacks, so a.theme-{color}class on the tip (viacustomClass) colours it; the default look is unchanged.
OTP input
- The plugin is enabled with a bare
data-coreui-otp.data-coreui-toggle="otp"no longer initialises anything. The toggle attribute marks a control the user activates — a button, a link, an input that opens a panel — and the OTP field is none of those: it is a wrapper the plugin builds its slots inside. Chip Input, Multi Select and the masked date and time fields already read that way. - A code delivered by SMS autofill or a password manager now fills the whole
field. Only the first slot carries
autocomplete="one-time-code"(the rest areoff), and a multi-character value arriving in any slot is spread across the field, as a paste already was. Previously such a value stayed in one slot and the submitted value was empty. The slots also gainedenterkeyhint,autocorrect="off"andspellcheck="false". - Breaking
Only the hidden input carries the field’s
name. Each visible slot was given<name>-<index>on top of it, so a six-slot field submitted seven keys where the option is documented as the name of the hidden input. The slots are unnamed now, and anameor anidwritten on a slot in the markup is left alone instead of being overwritten. The hidden input carries whatever the slots show, so slots rendered with their ownvalueattributes are submitted as well, which they were not before. - In a right-to-left field,
linearnow holds the forward move rather than theArrowRightkey. Forward isArrowLeftwhen the field reads right-to-left, so that is the key a linear field stops on an empty slot, andArrowRightwalks back freely. Previously the guard was attached to the key name, which let the caret jump ahead over empty slots in RTL and refused to walk back. - Breaking
The code is validated as one value. It moved from an
<input type="hidden">, which the browser leaves out of validation, to a text input laid invisibly over the group, withtabindex="-1"andaria-hidden="true", apatternfor the full length of the code and therequiredthat the slots used to carry. Arequiredcode that is empty or incomplete stops the submit, so does an incomplete code that is not required, and the browser puts focus on the first empty slot with its own message over the group. Once a validation reports the code, the group gets.is-invalidand every slot carriesaria-invalid="true"and is described by the message after the group while the code stays invalid. The stylesheet colours every slot and shows that message, which it never did for a slot marked invalid. Code that selectsinput[type="hidden"]inside the field needs.form-otp > input:not(.form-otp-control), a slot no longer getsrequired, and adata-coreui-invalid-feedbackwritten on the slots goes on the.form-otpelement. - The code takes a validation state. The new
validationStateoption, or an.is-invalid/.is-validclass on the.form-otpelement or on a slot in the markup, shows the state on the group and the slots, and'invalid'blocks the submit. A class on one slot, which used to colour only that slot, now marks the whole code. The user’s next change of the code drops it,setValidationState()sets it again, and a native form reset puts back the code the field started with. A change of the code sendsinputandchangefrom the input that holds the value. - Breaking
The
readonlyoption is removed, and every slot is editable. A code the user cannot type has no use in a form, sodata-coreui-readonly,{ readonly: true }and areadonlywritten on a slot no longer make it read-only; usedisabledto show a code the user cannot change.
Rating
- The read-only option is spelled
readonly. It was the one component in the library spelling itreadOnly, which made its data attributedata-coreui-read-onlywhile every other field component answers todata-coreui-readonly. Both spellings now agree with the HTML attribute:data-coreui-read-only="true"becomesdata-coreui-readonly="true", andnew Rating(element, { readOnly: true })becomes{ readonly: true }. The.readonlyclass the component writes is unchanged, and the React and Vue props stayreadOnly— that is how those frameworks spell the native attribute. dispose()takes back what the component rendered. The stars and their radios go away, so the form no longer submits the value; content written in the markup stays. A rating left in the page afterdispose()used to keep working as a plain radio group.- Its options decide
.disabled,.readonly, the size class, theradiogrouprole andaria-readonly,aria-requiredandaria-invalid. The same classes and attributes written in the markup are replaced at init, and arating-smorrating-lgwithoutsizeis removed.update({ disabled: false })used to leave.disabledbehind. - A read-only rating submits its value under the
nameyou give it.readonlyused to disable the radios, so the form left the value out; now they stay enabled, the mouse and the keyboard cannot change them, only the checked star takes focus, and the group carriesaria-readonly="true". A read-only rating withoutnamestill submits nothing. - A star with keyboard focus shows the focus ring.
- The setters keep the value the user picked and the stars.
setValue(),disable()andenable()change their part in place; any option change throughupdate()used to render the stars again and reset the rating to its initial value. - Breaking
The rating takes a validation state. The new
validationStateoption, or.is-invalid/.is-validon the rating element read once when it starts, colours the focus ring, and a given'invalid'setsaria-invalidon the group and blocks the submit until the user picks or clears a star or the form is reset. A class written on the element later is not read; set the state withsetValidationState(). See Validation. - The rating can be required.
requiredmakes every radio required and gives the grouparia-required="true". - Empty stars are
--cui-fg-4: gray-500 in the light scheme and gray-600 in the dark one, the lightest grays that keep 3:1 against the page background. The empty stars of v5 were 38% of the text colour, below 3:1 against the page in the light scheme.
Search Button
- Breaking
The shortcut handlers no longer create search button instances. Every
keydown, keyup and window blur anywhere on the page used to initialise each
[data-coreui-search-button], sodispose()lasted until the next key press. Now the buttons in the document atDOMContentLoadedare initialised, and a button added later, or one on a page whose script runs afterDOMContentLoaded, responds to its shortcut once you callcoreui.SearchButton.getOrCreateInstance(element). Activating such a button (a click, or Enter or Space on it) still initialises it. dispose()takes back the shortcut keys the component rendered. A.search-button-keyscontainer written in the markup gets its own content back.
Alert
-
Breaking Every alert fades out when dismissed, and
.fadeno longer decides that. The alert carries its own transition and--cui-alert-transition-property/-duration/-timing; the JavaScript marks it with.hidingfor the length of it, then removes the element. An alert is already on the page, so only the exit is animated —.fadeand.showremain the way to fade one in when you insert it at runtime. Add.alert-instantto an alert that should go at once, which is what an alert without.fadeused to do. Add.transition-sizenext to.fadeand the exit takes the box with it, so the page rises into the alert’s place instead of jumping. -
.alert-dismissibleis optional now. An alert that contains a close button as a direct child makes room for it and positions it on its own (.alert:has(> .btn-close)). The class keeps working — and is still the way to go when the button is injected later or sits inside a wrapper — so no existing markup changes.
Chip / Chip set / Chip input
- Breaking A chip starts selected only
from the
selectedoption. Anactiveclass in the markup no longer selects it; writedata-coreui-selected="true"on the chip instead. The role,tabindexandaria-*a chip or a chip set writes come from its options too, so a role ortabindexin the markup is replaced rather than kept, anddispose()leaves them in place for the next instance to rewrite. - A chip set is one tab stop. The set keeps
tabindex="0"on one chip and-1on the others and moves it with the focus, so Tab enters and leaves the set once and the arrow keys move between the chips. Every interactive chip used to be a tab stop of its own. - Chips expose real roles now. A selectable chip set is a horizontal
role="listbox"(witharia-multiselectablein multiple mode) whose chips arerole="option"elements carryingaria-selected; a standalone selectable chip is a toggle button (role="button") reflecting its state viaaria-pressedinstead ofaria-selected; a non-selectable set is arole="group".aria-disabledis only stamped on chips that carry a role — on a role-less chip the attribute was invalid and is now removed. clear()andremoveSelected()report the change once. Chip set and Chip input firedchangeandselect(.coreui.chip-set/.coreui.chip-input), and Chip input its nativeinputandchange, once for every chip removed; they fire once now, after the last chip is gone, and not at all when the chips did not change. The cancelableremovestill fires for every chip.- Adding and removing chips is announced. Chip set and Chip input announce
each change through
announce(); the wording is configurable viaariaAddedAnnouncement/ariaRemovedAnnouncement. - Every remove button names its chip. Chip and Chip set named each remove
button
Remove, so a set read the same name for every chip; the button isRemove <text>now, as Multi Select already did; Chip input names it after the chip’s text instead of its value.ariaRemoveLabel(and Multi Select’sariaChipRemoveLabel) takes a string, where{label}stands for the chip’s text and is appended when missing, or a function of the text for a different word order. A one-word label such asUsuńgains the text; a label that already spelled the text out, such asdata-coreui-aria-remove-label="Remove React", now repeats it, so drop it or use{label}. A.chip-removebutton you write yourself keeps itsaria-label, and gets one fromariaRemoveLabelwhen it has none. - A selectable chip hides its remove affordance. A chip with a role — a
selectable chip, which is a toggle button, or an option in a selectable set —
gets a
<span aria-hidden="true">instead of a<button>inside it, so its name is its text and a control is no longer nested in a button. Backspace / Delete remove it from the keyboard, the affordance from the pointer. - Chip input:
aria-disabled/aria-readonlyare no longer stamped on the container. The container is a generic element, so those attributes were invalid on it (axearia-allowed-attr); the nativedisabled/readonlystates on the inner<input>carry the semantics. - The remove button gets its size back, and
.chip-sm/.chip-lgresize everything they claim to..chip-removesized itself from a token that was never declared, so both dimensions were invalid at computed-value time and fell toauto— the button measured 0x0 at every chip size. It reads--cui-chip-remove-sizenow (16px, 14px on.chip-sm, 20px on.chip-lg), so a chip with a remove button is wider than it was. .chip-dismissis.chip-remove. The rename landed with the component; a single selector kept the old name and styled nothing, so a disabled chip input never reached its remove buttons through CSS. The button’sdisabledattribute was doing the work on its own.--cui-chip-dismiss-sizeis gone; use--cui-chip-remove-size. The size classes wrote the first name and the glyph read the second, so the remove icon stayed at its base size in every chip size. Nothing read the old token, so an override of it never did anything..chip-smand.chip-lgno longer swap the image and icon sizes. They passed the two tochip-size()in the wrong order, so a large chip rendered its icon at 24px and its image at 20px — the reverse of the base sizes, where the image is the larger of the two. Large is now 24px image / 20px icon and small 16px / 14px.- Breaking
The selected chip’s text follows the palette.
$chip-active-colorwas white at 87% opacity, computed at build time — repaint the selected background and the text stayed near-white. It defaults tovar(--cui-primary-contrast)now (solid, slightly brighter than before), and a.theme-*class supplies its own contrast colour through the slot. - Hovering a themed, unselected chip no longer previews the selected
look. The background steps from
bg-subtletobg-mutedand the text colour stays put; only selection paints the solid theme colour. A chip without a theme class hovers exactly as before.
Collapse
-
Breaking The
toggleoption is gone. It made the constructor change the state the markup declares, which surprised people — so nearly every caller passedtoggle: false. The constructor now always keeps the declared state; add.showto start open, and callshow(),hide()ortoggle()to change it.- const collapse = new coreui.Collapse('#myCollapse', { toggle: false }) + const collapse = new coreui.Collapse('#myCollapse')Callers that relied on the constructor toggling need an explicit call:
new coreui.Collapse(el).toggle().data-coreui-toggleon a trigger is unrelated and unchanged. -
Breaking Triggers no longer get the
.collapsedclass. Their state was written twice, as the class and asaria-expanded, and only the second is required. Nothing in the stylesheet had matched the class since the accordion stopped using it, so it survived as a hook for custom CSS — style the attribute instead, which is also what assistive technology reads:- .accordion-button.collapsed::after { transform: rotate(-90deg); } + [data-coreui-toggle="collapse"][aria-expanded="false"]::after { transform: rotate(-90deg); } -
New
hiddenUntilFoundoption. Withdata-coreui-hidden-until-found="true"a collapsed area is hidden withhidden="until-found"rather than taken out of the layout, so the browser’s find-in-page searches it and expands it on a match; the component then syncs.showandaria-expandedso the declared state follows what the reader sees. Off by default, and below Chrome 102 / Firefox 148 / Safari 26.2 it hides the content the wayhiddenalways did. -
Breaking The transition is set through CSS variables, and the Sass variables split in two.
$transition-collapseand$transition-collapse-widthwere shorthands carrying the property, the duration and the easing at once, which meant retiming a collapse required restating the property. They are replaced by$transition-collapse-property,$transition-collapse-durationand$transition-collapse-timing, emitted as--cui-transition-collapse-property/-duration/-timingon.collapseand.collapsing, so all three can also be changed per instance at runtime.$transition-fadesplits the same way into$transition-fade-duration/$transition-fade-timing. -
Breaking CSS owns the collapse animation, and the class choreography follows. Where
interpolate-size: allow-keywordsis supported the stylesheet interpolates the size to theautoof the open state on its own; elsewhere JavaScript measures the two sizes and the stylesheet still times the move. Three observable changes come with that:.collapsenever leaves the element. It used to be swapped out for.collapsingwhile the transition ran; now.collapsingis added alongside it, so.collapse.collapsingmatches during a run and a selector keying on the absence of.collapseno longer fires mid-animation..showtoggles at the start of both directions. It used to land at the end of the opening transition. A rule styling.collapse.showapplies for the whole expansion now, and its absence marks the whole closing run.- An open
.collapse-horizontalisfit-contentwide, not as wide as its container — the open size is the animation’s target, and a target ofautowould spend most of the run growing an empty box past the content. This also removes the end-of-animation jump to full container width the measured animation used to make. Style the child, as the docs always showed, and nothing changes.
Button
-
A toggle button always exposes
aria-pressednow. The attribute has no default value, so a[data-coreui-toggle="button"]that shipped without one was announced as a plain button until the first click. JavaScript adds the missing attribute onDOMContentLoaded, reading the.activeclass to pick the value; an attribute you wrote yourself is left alone. Write it in your markup anyway, so the state is correct before our JavaScript runs. -
Breaking
.btnis a flex container. It lays its content out in a row, centres it on both axes and separates the parts with--cui-btn-gap, so an icon next to a label no longer depends on a whitespace text node in your markup — and no longer gets a second space where you did leave one.--cui-btn-min-heightkeeps a button that holds only an icon as tall as one that holds text. Custom CSS that positioned button content withdisplay,line-heightor margins needs a second look; the caret of a.dropdown-togglenow takes its distance from the gap instead of its own margin. -
Breaking A button label no longer wraps.
--cui-btn-white-spaceisnowrap. In v5$btn-white-spacewasnull, so no declaration was emitted at all and a long label broke onto a second line, leaving the button taller than the ones beside it — while the variable’s own comment told you to setnowrap, which is to say the default was the one nobody wanted. A long label now keeps the button one line tall and makes it wider instead. -
.btn-toolbarspaces its groups on its own with a.5remgap(--cui-spacer-3). Drop theme-*utilities that carried the spacing in v5, or they now add to the gap.It is still a token, so the old behaviour is one declaration away — on a single button, on a theme, or globally:
.btn-that-wraps { --cui-btn-white-space: normal; } :root { --cui-btn-white-space: normal; }In Sass,
$btn-white-space: normal. The token is declared on.btnitself, so an override has to reach the button — setting it on an ancestor is not enough.Look at rows that hold several buttons: a flex row without
.flex-wrapgrows wider now instead of taller, so add it where the group should break onto a second line rather than push the layout out. -
.btn-icon(new). A button that holds nothing but an icon: no padding, and its width follows its height, so it stays square at every size. -
A variant is a shape, the colour is a theme.
.btn-solid,.btn-outline,.btn-subtle(new) and.btn-ghosttake their colour from a.theme-*class on the button or on any element above it, and each carries the base button styles on its own —.btnis no longer required next to them:- <button class="btn btn-primary">Save</button> + <button class="btn-solid theme-primary">Save</button>The colour classes keep working and stay the spelling the docs use:
.btn-primaryis.btn-solid.theme-primary,.btn-outline-primaryis.btn-outline.theme-primary, and.btn-subtle-*completes the set. What changes is that one rule per variant now serves every colour, so a theme colour of your own gets the whole set for free. -
Breaking A bare
.btnis a neutral button, not an invisible one. It renders in the surface colours (--cui-bg-2, one step darker on hover) instead of being transparent and unstyled. Buttons that carry a variant or a colour class are unaffected. For the old behaviour use.btn-ghost, which is transparent by design. -
.btn-xs(new). A fourth size below.btn-sm, reading the shared--cui-btn-input-xs-*tokens. -
The resting button shadow is gone.
$btn-box-shadowand--cui-btn-box-shadoware removed; a.btndraws nobox-shadowat rest even with$enable-shadows: true, only the pressed state keeps its inset (--cui-btn-active-shadow). The removed default was the v4 bevel (inset 0 1px 0 rgba(white, .15), 0 1px 1px rgba(black, .075)); put it back with your own.btn { box-shadow: … }if you want it.
Loading button
- Breaking
disabledOnLoadingno longer sets thedisabledattribute. A disabled button drops focus, so a keyboard or screen reader user who pressed it landed on the page body. While loading, the button now getsaria-disabled="true"and.disabled, keeps focus, and ignores clicks and form submission until it stops. CSS or scripts that read:disabledon a loading button should read[aria-disabled="true"]instead. - The start and the end of loading are announced when the button has
focus: its name followed by the new
ariaLoadingLabeloption, then its name again. While loading, the spinner is arole="img"named byariaLoadingLabelinstead of an emptyrole="status".
Navbar
- Breaking
.navbar-expand-*reads the navbar’s own width, not the viewport’s. Every.navbaris a query container now, so a navbar in a sidebar or a narrow dashboard column collapses there while a full-width one on the same page stays expanded. For a navbar that spans the page — which is most of them — nothing changes, because its width is the viewport’s. flex-wrap: nowrapandjustify-content: flex-startmoved from.navbar-expand-*onto its inner.container*. A container query styles descendants of the container, never the container itself, and the inner container is where those two properties took effect anyway (the navbar’s children inheritflex-wrap). A navbar written without an inner.container,.container-fluidor.container-{breakpoint}no longer gets them — wrap its content, the way every example in these docs does.--cui-navbar-text-padding-yand--cui-navbar-toggler-icon-sizeare new. The padding that lines.navbar-textup with the nav links, and the size of the hamburger glyph, were both compiled into their rules. The padding could not be matched from outside at all: the value it pairs with,--cui-nav-link-padding-y, is declared on.navbar-nav, which is.navbar-text’s sibling and therefore invisible to it. Both are declared on.navbarnow, at their current values.- An expanded navbar flattens a
.drawer, not just an.offcanvas. The reset that turns the collapsed panel back into an inline nav named.offcanvasonly, so a.drawerinside a.navbar-expand-*stayed a fixed side panel over the page at every width — measured at 400×868 next to the offcanvas’s 1176×24. Both are handled now. - Breaking
Three navbar defaults move.
.navbartakes--cui-navbar-min-height: 3.5rem, so a navbar with short content no longer collapses below the height of one with a brand in it..navbar-brandtakes--cui-navbar-brand-font-weight, defaulting to medium rather than the body weight..navbar-navtakes--cui-nav-gap, a.25remgap between links that the link padding used to carry alone. Set any of the three to its old value to keep the previous rendering. .navbar-translucentis new. It tints and blurs the navbar the way.toast-translucentdoes for a toast, mixing--cui-navbar-bg— a new token, defaulting to the body background — at--cui-translucent-opacity.- A theme beats a navbar token, as everywhere else.
--cui-navbar-brand-color,-brand-hover-colorand-active-colorcarried the--cui-theme-*slot inside their own default, so setting one of them inside a.theme-{color}navbar dropped the theme. The slot sits on the property now, so the theme wins — the same order every other component follows.
Checkbox, radio and switch: the class moved onto the input
Breaking.check, .radio and .switch go on the <input> itself. There is no wrapper
and no second element: appearance: none lets the control render a
pseudo-element, so the mark is drawn on the input, and its colour is a token
(--cui-check-mark-color) rather than a value baked into an SVG. The
separate dark-mode switch artwork is gone with it — a thumb painted from a token
follows the colour scheme by itself.
- <div class="form-check">
- <input class="form-check-input" type="checkbox" id="c">
- <label class="form-check-label" for="c">Label</label>
- </div>
+ <div class="form-field">
+ <input class="check" type="checkbox" id="c">
+ <label for="c">Label</label>
+ </div>The v5 spelling still works. .form-check, .form-check-input,
.form-check-label, .form-switch and .form-check-inline are kept as a
compatibility layer — aliases of the new controls, not a second
implementation, so there is one engine to maintain and the old markup lays out
exactly as it did. Verified by measuring geometry across six markup variants and
eight dimensions each: zero differences. It is deprecated in v6 and removed in
v7. It ships in coreui-legacy.css, not in coreui.css, so a build whose markup
has moved leaves the sheet out and carries no .form-check* selector at all.
.btn-check on the <label> is a current feature and lives with the buttons.
The v5 spelling — .btn-check on the <input>, with a sibling <label class="btn"> — is in coreui-legacy.css alongside the rest.
The wrapper it draws is styled from scss/_legacy.scss and goes when the
layer does.
Five Sass variables shaped the old artwork and cannot be honoured any more,
because the compatibility layer draws through the new engine:
$form-check-input-checked-bg-image, $form-check-radio-checked-bg-image,
$form-check-input-indeterminate-bg-image and $form-check-transition. Their
replacements are --cui-check-mark-checked,
--cui-radio-mark, --cui-check-mark-indeterminate and the
controls’ own transition tokens. $form-check-label-color and
-cursor keep working. $form-check-btn-check-disabled-opacity goes with the
toggle button’s move to the buttons — it duplicated $btn-disabled-opacity,
which is now what the disabled toggle reads, through
--cui-btn-disabled-opacity.
Removed
.form-check-reverse is gone. It right-flushed the label and the control as
a pair — which order utilities express directly — and it was never the settings
row (label on the left, control on the right) that people actually reach for;
that one is justify-content: space-between. The React and Vue reverse prop
still emits the class and therefore stops having an effect; it is retired with
those components in the framework editions.
What replaces the wrapper’s modifiers:
| v5 | v6 |
|---|---|
.form-check (layout) | .form-field, or nothing if you only need the control |
.form-check-label | a plain <label> |
.form-check-inline | a flex row: .d-flex.gap-4 |
.form-check-reverse | justify-content: space-between on the field, or order utilities to swap the pair |
.form-switch-lg / -xl | .switch-lg / .switch-xl |
The switch is sized by one token, and gains .switch-sm. --cui-switch-width
is derived from --cui-switch-height through the new
$switch-aspect-ratio — a plain number, multiplied in the browser — so a size
sets the height alone. The size classes now
scale the label as well, matching .check-sm / .radio-lg. $switch-sizes
replaces the width/height pairs, and the deprecated .form-switch-lg / -xl
read it too — $form-switch-widths is gone. Those two classes render exactly
as .switch-lg / .switch-xl now, so they grow a little: the track was
1.25em / 1.5em tall on a 2:1 ratio, and is 1.5rem / 1.75rem on the library’s
1.75 ratio.
Fixed: the switch animates again. --cui-switch-transition carried a
property name inside it (background-position .15s ease-in-out, inherited from
v5’s $form-switch-transition), so transition: inset-inline-start var(…)
resolved to something invalid and every switch transition was dropped. It is two
tokens now — --cui-switch-transition-duration and
-timing — and the thumb slides rather than the background position.
Breaking
The switch is redrawn. The track is 1.25rem tall with a 1.75 aspect ratio
(was a 2:1, font-relative em track), pill-radiused, on the tertiary surface
with an inset shadow; the thumb is white with a drop shadow and turns
contrast-coloured when checked. A disabled unchecked switch swaps to the
disabled surface with a faded thumb instead of dimming wholesale; a checked one
dims. --cui-switch-border splits into -border-width and
-border-color, and $form-switch-color, $form-switch-width and
$form-switch-border-radius fed only the old look and are removed. The
deprecated .form-switch-lg / -xl are redrawn with the rest.
The controls animate on shared knobs. :root carries
--cui-control-transition-duration and -timing (an overshoot easing);
the checkbox tick and the radio dot pop in on them, the boxes’ colours animate,
and the switch thumb slides on the same pair — the three controls move in sync
and restyle from one place. Reduced motion disables all of it as usual.
New: checkbox and radio have sizes. .check-sm / .check-lg and
.radio-sm / .radio-lg — v5 had none, only the switch was sizeable. Each sets
one custom property; the mark scales with the box and the alignment margin is
computed from the same value, so any size you set yourself stays on the text’s
optical centre too.
Multi Select’s option indicators moved with it — they render as a decorative
.check now, so a build with the compatibility layer off still draws them.
Checkbox, radio and switch: one page became four
-
Each control now owns its custom properties.
--cui-checkbox-*,--cui-radio-*and--cui-switch-*feed the shared implementation at runtime, so restyling checkboxes no longer drags radios along and no longer needs a Sass rebuild. Set them on the control — a token an element declares for itself cannot be overridden from an ancestor. The old--cui-form-check-*properties still exist and still work; they are the shared layer underneath. -
The control stays on the text baseline when you resize it. Its
margin-topused to be Sass arithmetic over$form-check-input-width, so changing the width token at runtime moved the box off the line. It is acalc()over the size token now. -
Sized switches are token overrides.
.form-switch-lg/-xlset--cui-switch-widthand--cui-switch-heightinstead of writing their own geometry, so a size the map does not name is two custom properties rather than a Sass recompile. -
The Sass variables behind all of this moved into
scss/forms/_checkbox.scss,_radio.scssand_switch.scss.$form-check-*and$form-switch-*keep working — the new per-control variables default to them. -
The docs split.
/forms/checks-radios/is now Checkbox, Radio and Switch — one page per control, each self-contained down to its own toggle-button and Sass sections. The old URL redirects to Checkbox. No classes changed:.form-check,.form-check-input,.form-check-label,.form-switchand.form-check-inlineall work exactly as before;.form-check-reverseis removed. -
New:
.form-field, a grid wrapper that lays a control out with its label, description and validation feedback on one consistent gap, so a form is spaced by the field instead of by margin utilities on its parts. A bare.form-check-input(one written without its.form-checkwrapper) switches the field to a two-column layout through:has().
Stacks
- New:
.hstack{-sm|-md|-lg|-xl|-2xl}and.vstack{-sm|-md|-lg|-xl|-2xl}switch direction at a breakpoint. The breakpoint is the width of the nearest query container, so mark an ancestor with the new.stack-container(or the.contains-inlineutility) — without one the direction never switches. Plain.hstackand.vstackare unchanged, including.hstack’s vertical centring and its shrink-to-content width.
Images and figures
--cui-thumbnail-*and--cui-figure-caption-*are declared, not just read. They existed only as the fallback arm of avar()call, so nothing reported them and an override had to guess the name..img-thumbnailand.figure-captiondeclare their full set on the element, padding, border width and caption font size included.--cui-figure-img-margin-bottomis new. The gap between a figure’s image and its caption was compiled into the rule, so the only way to change it was to rebuild from Sass..figure-imgdeclares it now, and it is the one figure measurement you can retune in the browser.
Figures
- Breaking
.figureis an inline flex column and spaces its parts with--cui-figure-gap.$figure-img-margin-bottomand--cui-figure-img-margin-bottomare gone — the gap (knob:$figure-gap, same.5rem) replaces the image’s bottom margin, and.figure-imgcarries no styles at all any more — keep it in your markup or drop it, both render the same. Documented markup renders pixel-identically; an<img>without width/height attributes can now stretch to a wider caption (flexstretch), so give figure images explicit dimensions — which they need anyway to avoid layout shift.
Reboot
- Links change color on hover.
--cui-link-hover-colorwas declared and read by navs and pagination, but a plain<a>only ever changed its underline. It now switches color as well, and inside a themed surface it hovers to--cui-theme-fg-emphasis. - The underline sits
.2embelow the text.<a>reads the new--cui-link-underline-offsettoken (knob:$link-underline-offset), so underlines clear descenders instead of cutting through them. The.underline-offset-*utilities and.icon-linkstill override it per element; set the token toautofor the old browser default. accent-color: var(--cui-primary)on:root. Native controls we don’t restyle — a bare checkbox, radio, range or<progress>— follow the brand color instead of the browser’s blue.- Every segment of a date or time input gets its padding removed. The list
covered five pseudo-elements and one of them (
::-webkit-datetime-edit-minute) had been renamed by browsers years ago; seconds, milliseconds, the week number and the AM/PM field were never in it.<input type="time" step="1">no longer renders taller than a text input. tel,url,emailandnumberinputs stay LTR everywhere. The rule was scoped to[dir="rtl"]elements, so it missed pages that set the direction on<html>through the language rather than the attribute.prereserves scrollbar space (scrollbar-gutter: stable), so a horizontal scrollbar no longer covers the last line of a code block.- Reboot’s type and spacing are CSS variables on
:root. Heading margin, family, style, weight and line height, paragraph margin,hr,legend,dt,sub/sup,markpadding, the code font size and--cui-list-padding-xare declared rather than compiled in, so a subtree can be retyped without a class.<kbd>carries its own set on the element,--cui-kbd-border-radiusincluded. .h1–.h6,.small,.markand.initialismare defined in Reboot, next to the elements they mirror, instead of@extend-ing them from_type.scss. Only.initialismchanges cascade layer (content→reboot); the others were already emitted there, because@extendhoists a class to wherever the extended selector is defined._type.scssis gone with.leadand.display-*(see the type scale below).- Hardcoded spacing in Reboot now reads the
--cui-spacer-*scale (address,ol/ul/dl,dd,blockquote,pre,figure). Values are unchanged.
Floating labels
- The label may now come first.
.form-floatingmatches the control’s state in both directions — forward from the label with:has(), backward from the control with~— so<label>before the control floats and disables exactly like the v5 order, which keeps working. Writing the label first lets a screen reader announce it before the field’s value, and the docs examples do. - A textarea’s label stays at the top. The label is a flex box centred on the
control, and
label:has(~ textarea)anchors it to the start instead, so it floats correctly however tall the textarea is. - Breaking
The placeholder shows once the field has focus. It used to stay
transparent in every state; it now appears under the floated label while the
field, or the control inside a field component, has focus, and stays hidden
while the label rests in the field. A read-only
.form-control-plaintextkeeps it hidden. - New:
.form-floating-alwayskeeps the label floated and the placeholder showing while the field is empty. It works on.form-floatingor on any ancestor. See Always floating.
Lists
--cui-list-inline-paddingis new. The gap between.list-inline-items was compiled into the rule, so respacing an inline list meant rebuilding from Sass..list-inlinedeclares it, at the same.5rem.
Tables
.table-responsive*is a query container. It still scrolls horizontally below its breakpoint; the addition is that things inside it can now respond to its width.- New:
.table-stacked{-sm|-md|-lg|-xl|-2xl}turns rows into stacked blocks below the breakpoint, reading the width from the.table-responsive*wrapper. Give every<td>past the first adata-cellattribute for its label; the header is hidden visually and kept in the accessibility tree. See Stacked tables. - New:
.table-cardwraps a table in a card’s border and corner radius, clips the cells, and drops the table’s bottom margin and the borders that would stack on the frame. It goes around the table or on its.table-responsive*wrapper. See Card-like tables. - Cell padding, vertical alignment and the border width are CSS variables
now.
--cui-table-cell-padding-y,--cui-table-cell-padding-x,--cui-table-cell-vertical-align,--cui-table-border-widthand--cui-table-group-separator-colorare declared on.tableinstead of being compiled into the cell rule. This also fixes stacked tables, which read--cui-table-cell-padding-*and--cui-table-border-widthwhile nothing declared them, so their rows collapsed to zero padding and lost the separating line. .table-smsets the padding variables on the table instead of overriding the padding on every cell. Anything that raised specificity to beat.table-sm > :not(caption) > * > *can set--cui-table-cell-padding-y/-xinstead.- Breaking
The contrast factors are percentages.
$table-striped-bg-factor,$table-active-bg-factorand$table-hover-bg-factorchanged from unit-less fractions (.05) to percentages (5%), because they now ship as the--cui-table-*-bg-factorvariables thatcolor-mix()reads at runtime. - The state colours follow
--cui-table-color.--cui-table-striped-color,--cui-table-active-colorand--cui-table-hover-colordefault tovar(--cui-table-color), and the matching backgrounds are mixed from it, so a variant only has to set--cui-table-color,--cui-table-bgand--cui-table-border-color.
Blockquote
- Breaking
The source line reads
--cui-secondary-color..blockquote-footerwas painted with--cui-gray-600, a fixed value that never flipped with the colour mode: 4.15:1 on the light background and 3.65:1 on the dark one, both under the 4.5:1 minimum. The secondary colour is defined per mode and clears the threshold in both. - The quote lays its children out with flex and
gap. Spacing inside a multi-paragraph quote is--cui-blockquote-gaprather than each paragraph’s own bottom margin, and.blockquote > :last-child { margin-bottom: 0 }is gone with the margins it existed to cancel. - The footer no longer pulls itself up with a negative margin. A quote
followed by
.blockquote-footerdrops its own bottom margin instead (:has()), so the two spacings can be set independently:--cui-blockquote-margin-yand--cui-blockquote-footer-margin-y.
Toast
- New:
Toastercreates toasts from options and keeps them in a store keyed by id —add()returns the id (an existing id updates in place, a closing one is replaced),update()rewrites a toast,close()hides one or all,promise()follows an asynchronous operation through loading, success and error. It creates its own.toast-containerplaced byplacement, caps visible toasts withlimit(the oldest stay in the DOM,inertand hidden), announces each toast throughannounce()bypriorityand can render an action button. Toasts slide in from beyond the container’s edge and back out, toasts over thelimitfade out instead of vanishing, and the toaster glides the remaining toasts into place when one leaves;stack: truepiles them up, newest in front, and spreads them on hover or focus. Nine.toast-container-{block}-{inline}classes pin a container to the viewport in place of theposition-fixed top-0 end-0utility stacks. New classes:.toast-title,.toast-description,.toast-action. - Breaking
The fade moved to CSS. Toast was the last component driving its animation
from JavaScript — it toggled
.fade, forced a reflow, then juggled.show,.showingand a deprecated.hideclass to fake an exit. Menu and the picker popup already express this with@starting-styleand a discretedisplaytransition, and Toast now does the same. JavaScript only toggles.show.- The
animationoption anddata-coreui-animationare gone. Add.toast-instantto skip the animation. .showingand the deprecated.hideclass are gone, and toasts no longer get the generic.fadeclass. Replace any CSS or tests that depend on them.- The transition is tuned with
--cui-toast-transition-durationand--cui-toast-transition-timing. isShown()returnsfalseas soon ashide()is called, rather than when the fade-out ends.
- The
- Breaking
Toasts are opaque by default.
--cui-toast-bgand--cui-toast-header-bgresolved tocolor-mix(in srgb, var(--cui-body-bg) 85%, transparent), so every toast let 15% of whatever sat behind it through — with no blur to settle it, text underneath read straight through the message. Both now resolve tovar(--cui-body-bg). Add.toast-translucentfor the see-through look, which putsblur(5px) saturate(180%)behind the toast and thins the header to match, the same way.card-translucentand.menu-translucentwork. - New:
.toast-container-{top|middle|bottom}-{start|center|end}. Nine classes pin a.toast-containerto the viewport (position: fixed), inset from the edge by--cui-toast-container-inset(--cui-spacer-5, 1rem, by default) and centred with logical insets and auto margins rather than transforms, so they hold in RTL. They replace theposition-fixed top-0 end-0 p-3utility stack; inside a positioned box add.position-absoluteto pin the container to the box.--cui-toast-container-insetis declared on.toast-containernext to--cui-toast-zindex.
Card
- Breaking
A card is roomier and framed in the solid border colour.
--cui-card-spacer-y/-xgo from1remto--cui-spacer-6(1.25rem), and the image-overlay padding follows them. The header, the footer and a list group placed directly in the card take--cui-card-spacer-xas their inline padding, so their text lines up with the body; the header and footer keep.5remof block padding. The border reads--cui-border-colorinstead of--cui-border-color-translucent, except on.card-translucent, and the radius is--cui-radius-7(see the radius scale below). A card filled with.text-bg-*keeps that neutral frame; add the matching.border-{color}to frame it in the fill colour. To keep the v5 spacing, set--cui-card-spacer-yand-xtovar(--cui-spacer-5)on.card. - Breaking
.card-header-pillsis gone. It only pulled the pill row out to the header’s padding edge, which is wrong for pills — they carry their own background, so flush against the header’s inner edge they touch the border. Drop the class and keep.nav.nav-pills. - The footer follows the theme colour like the header does. A themed card
(
.card.theme-inverse, say) used to paint the footer’s background from the theme while leaving its text at the body colour, so the label sank into the fill. The footer now reads--cui-theme-contrastwith--cui-card-cap-coloras the fallback, the same expression the header already used. - Every card token now carries a real value.
--cui-card-title-color,--cui-card-subtitle-color,--cui-card-cap-color,--cui-card-color,--cui-card-heightand--cui-card-box-shadowshipped as empty declarations (--cui-card-title-color: ;), because their Sass variables werenulland interpolatingnullyields an empty string rather than dropping the entry. An empty custom property makes everyvar()that reads it invalid at computed-value time, so the rendered result happened to be right, but the token read as blank in devtools and could not be told apart from one you had cleared yourself. They now shipinherit,autoandnone, which compute to the same thing and make the customization surface inspectable. - The last card in a
.card-groupgets its bottom-left corner squared. The rule targeting it was written> card-footer, without the leading dot, so it matched an element type that does not exist and never applied. - A theme class works on a single section, not just the whole card.
<div class="card-header theme-primary">used to paint white text on a neutral header, because the background came from--cui-card-cap-bg, a token declared on.card— a custom property resolves where it is declared, and--cui-theme-basewas set one level below it, on the header. The theme indirection moved to the point of use, so each section reads the theme in its own scope. A theme class on the.carditself behaves exactly as before. - Breaking
.card-bodyis a flex column. It spaces its children with--cui-card-body-gapand zeroes their block margins, so nothing in a card needs.mb-*to sit correctly any more. Two consequences for existing markup: a direct child takes its natural width rather than filling the card (add.w-100where a block child has to stretch), andtext-alignno longer moves a button or a link, because they are flex items — use.mx-autoor.ms-autoinstead. - Stacked sections are divided by a single rule. A body, a list group or a
bottom image cap that follows an image cap, a body or a list group draws a
divider above itself in the card’s own border, so two bodies in a row, or an
image cap above a body, no longer sit with nothing between them, and a border
utility or a theme on the card carries over to every divider. A section hidden
with
hiddenor.d-nonedoes not count. A header and a footer draw their own divider, as before. .card-groupsquares the corners at each seam along the writing direction. The rules named the physical corners, so a right-to-left group squared its outer corners and rounded the ones that meet. A list group at the edge of a grouped card is squared like the other sections.- Breaking
.card-linkis gone, along with--cui-card-title-spacer-y. Both existed to space children the gap now handles; the class’s inline margin only ever worked because the links were inline. Wrap them yourself:<div class="hstack gap-4">…</div>. - New:
.card-rowlays a card out along the inline axis, with.card-img-startand.card-img-endfor an image cap on either side. It replaces the.row.g-0+.col-md-*recipe, which needed a wrapper per section and drew no divider between them. - New:
.card-subtleswaps the theme’s solid fill for its subtle background, border and emphasis text. - New:
.card-translucentblurs and saturates whatever is behind the card and thins out the caps to match. The mix and the filter are the shared--cui-translucent-*tokens; the caps read--cui-card-translucent-cap-bg-opacity(Sass default$card-translucent-cap-bg-opacity). - Breaking
.card-groupattaches its cards from a container query, not a media query. Thesmbreakpoint is now the width of the nearest query container, which is what a group inside a column actually has to fit — the viewport being wide never meant the group was. Mark an ancestor with.contains-inline; without a container the query has nothing to read and the cards stay stacked at every width.
List group
- Breaking
.list-group-horizontal-{sm|md|lg|xl|2xl}turns horizontal from a container query. Same change and same fix as.card-group: put.contains-inlineon an ancestor, or the list stays vertical. Plain.list-group-horizontalis unconditional and needs no container. - An active item under a
.theme-*class is filled, like a page link..list-group-item.activereads--cui-theme-contrastfor its text and--cui-theme-bgfor its background and border — the same slots the pagination, the badge and the button use — so the active item of a.theme-dangerlist is the same red as the active page of a.theme-dangerpagination. A pressed action (:active) reads--cui-theme-bg-muted, like hover. Without a theme class nothing changes.
Tab
-
Breaking A tab pane fades on its own —
.fadeon a.tab-panedoes nothing for it..tab-panecarries the transition and--cui-tab-pane-transition-property/-duration/-timing, and@starting-stylesupplies the state to fade from. Drop.fadefrom your panes; add.tab-pane-instantto a pane that should switch without one..fadeitself stays as a generic helper for your own markup. -
The pane no longer waits a full transition before it starts fading. The JS added
.showfrom inside the callback it had queued on the pane’s own transition — so it waited out a fade that had not begun, then began it. Both classes land in the same frame now, andshownwaits on the pane, the element that actually animates, instead of the tab button, which only transitions its colours.
Modal
-
Breaking A size modifier applies at every viewport.
.modal-lgand.modal-xlwere withheld until thelgandxlbreakpoints, so on a tablet a large modal was 500 px — the same as one with no modifier at all — and an extra-large modal on a 1100 px screen was 800 px. Both ask for their width everywhere now, capped bymax-width: calc(100% - margin * 2)exactly as before, which is what kept horizontal scrollbars away in the first place. At 768 px.modal-lggoes 500 → 712 px, and at 1100 px.modal-xlgoes 800 → 1044 px. -
Breaking
.modal-smis narrow on phones too. It used to fall back to the default width below thesmbreakpoint, so on a 360 px screen it filled the width; it is 300 px there now. Drop the modifier for markup that should fill a small screen. -
$modal-sizesreplaces the three breakpoint blocks. Sizes come from a map and a loop, the same shape.dialog-*already used — one model for both windows instead of two.
Pagination
-
Breaking A page link is sized from the shared control scale. Pagination kept its own padding values, so
.pagination-lgdid not match.btn-lgor.form-control-lg— it measured 56 px against their 48. The tokens read--cui-btn-input-*now,.page-linkis a centred flex box with--cui-pagination-min-height, and all three sizes line up. The base and small sizes are unchanged; the large one loses 8 px of height and some horizontal padding. -
Breaking
pagination-size()is removed, together with$pagination-padding-{x|y}-{sm|lg}and$pagination-border-radius-{sm|lg}. Sizes are generated by a loop over$pagination-sizes; add an entry to it and define the matching--cui-btn-input-<size>-*values instead of calling the mixin. -
Keyboard focus keeps the theme colours.
:focus-visibleread--cui-pagination-focus-colorand-focus-bgwith no theme slot, so tabbing to a link inside.theme-successdropped it back to the plain link colour. Both now read--cui-theme-fg-emphasis/--cui-theme-bg-subtlefirst, like every neighbouring state. -
$pagination-margin-startis removed. The overlap between links iscalc(-1 * var(--cui-pagination-border-width)), so it follows a border width changed at runtime instead of the value frozen at build time. The Sass conditional that compared the variable against that expression is gone with it.
Placeholder
- The opacities are CSS variables.
--cui-placeholder-opacity-maxand--cui-placeholder-opacity-minon.placeholderset the resting opacity and the pulse, and--cui-placeholder-wave-opacity-maxand-minon.placeholder-waveset its highlight, so both can be retuned on an instance. They were compiled in from$placeholder-opacity-maxand-min. - Placeholders are paler, taller and rounded. The resting opacity is
.18(was.5) and the pulse dips to.1(was.2). The default height is1.25em(was1em), capped a quarter em below the line height so a placeholder never makes a heading or a tight line taller;.placeholder-xsis.75em(was.6em),.placeholder-sm1em(was.8em) and.placeholder-lg1.75em(was1.2em). Each placeholder has a--cui-radius-3corner. A button, avatar, badge or chip that takes.placeholderas a modifier keeps its own height and corners. .placeholder-waveis a sweeping highlight and the recommended loading animation. A narrow bright band crosses the placeholders every 1.6s instead of a wide gradient every 2s, and it also works on text. The opacity pulse is.placeholder-pulsenow;.placeholder-glowstays as its alias. The keyframes are renamed with it, so custom CSS that namesanimation: placeholder-glowneedsplaceholder-pulse. With reduced motion the wave stops and drops its mask.
Popover and Tooltip
-
Deprecated
placementtakes the logicalstartandend;leftandrightare deprecated. The physical names never meant a physical side — both components have always mirrored them in RTL, which their own pages documented — sorightwas already “the end of the reading direction” wearing the wrong name. The new spellings say it, and match the ones Menu and Dropdown already take. Nothing breaks:leftandrightkeep working and resolve identically, and they are removed in v7. -
The fade moved to CSS. Neither component renders the
fadehelper class any more;.showalone drives the transition, with@starting-stylesupplying the state to animate from.animation: falserenders.popover-instant/.tooltip-instantinstead of omittingfade, and a customtemplateopts out with the same class. The option itself is unchanged; only custom CSS targeting.popover.fadeor.tooltip.fadeneeds updating. Tune the motion with--cui-popover-transition-duration/-timingand their tooltip counterparts. -
A bottom-placed popover’s arrow is filled like the header it points into. The arrow sits above the header and was drawn in the body background, so it read as a notch cut out of the header strip. It takes
--cui-popover-header-bgwhen a header follows it, and stays on--cui-popover-bgwhen there is none. -
--cui-popover-header-colorreads--cui-heading-color. It resolved$headings-colorat build time, so a runtime override of the heading colour skipped the popover header. The default is unchanged.
Progress
-
Breaking
$progress-bar-animation-timingis replaced by$progress-bar-animation, which carries the keyframes name too. The stripe animation is a CSS variable now —--cui-progress-bar-animation— so its speed, easing and keyframes can be changed on an instance instead of only at build time. A Sass override that passed just the timing (1s linear infinite) has to pass the whole shorthand (progress-bar-stripes 1s linear infinite). -
The label font size follows
--cui-font-size-sm. It was three quarters of the body font size, a value only this component used; it is the shared small size now, which makes it one step larger. Set--cui-progress-font-sizeto go back. -
Breaking
.progress-group,.progress-group-prepend,.progress-group-headerand.progress-group-barsare removed. Four classes carried a flex row, a 100 px label column, a header row and a 2 px gap between bars — all of which utilities already do. Use.hstack/.vstackwith agap-*and put the label column’s width in a style, as the Grouping bars examples now show. -
Breaking
.progress-thinis removed. It did nothing an inline height does not. Usestyle="height: 4px"on the.progress, as the Height section shows, or set--cui-progress-heightif you want the stripe geometry to follow the height too. -
.progress-whitesets tokens. It was paintingbackground-colorpast--cui-progress-bg, so nothing could adjust it. It now sets--cui-progress-bg(translucent white) and--cui-progress-bar-bg(--cui-white) and lets the base rules paint; the bar stays white under a.theme-*class, as the variant is meant for dark and brand-coloured surfaces. -
Gradient bars.
--cui-progress-bar-bg-image(defaultnone) paints the bar with an image on top of its colour, so a gradient is one variable on the.progressor the bar. The stripes moved to their own tokens for the same reason:--cui-progress-bar-stripesholds the pattern and--cui-progress-bar-stripe-colorits colour, and.progress-bar-stripedlayers the pattern over the image. Theprogress-bar-stripeskeyframes now shift only the first background layer, so the image underneath stays put. -
.progress-segmented. A mask that cuts a.progressor.progress-stackedinto a row of rounded segments,--cui-progress-segment-min-widthwide and--cui-progress-segment-gapapart — or exactly--cui-progress-segmentsof them when that is set — with corners from--cui-progress-segment-border-radius(--cui-radius-2by default,0pxfor square). Everything under the mask — transition, theme, gradient, stripes — behaves as before. -
New
Progressplugin. Initialized on every.progress-segmented, it reads the bar’saria-valuenow,aria-valueminandaria-valuemaxand sets--cui-progress-segments-filled, which the stylesheet turns into the bar’s width, so the fill ends on a whole segment. It also counts how many segments of--cui-progress-segment-min-widthfit the track, capped at--cui-progress-segmentswhen set, and follows the track as it resizes; a script that changes the bar’saria-valuenow,aria-valueminoraria-valuemaxcallsupdate(). An inlinewidthon the bar still wins over the segment fill.coreui.Progressis exported next to the other plugins.
Spinner
-
Breaking Spinners are sized in
em, notrem..spinner-borderand.spinner-groware now1emsquare with a.125emborder, so a spinner takes the font size of whatever it sits in instead of being a fixed 32 px block. Inside a button or a paragraph it lands at the size you previously had to ask for with a modifier; standalone it is half of what it was. Set--cui-spinner-width/-height, or the$spinner-width/$spinner-heightSass variables, to go back to a fixed size. -
Breaking
.spinner-border-smand.spinner-grow-smare replaced by one.spinner-sm. The small variant no longer has to correct the border width, so a single class serves both spinners. -
Breaking
--cui-spinner-vertical-alignis gone. Spinners areinline-flexand assume a flex parent; the-.125embaseline nudge it used to need no longer applies. -
The animation timing function is a token.
.spinner-borderkeepslinear, while.spinner-grownow eases withease-in-outinstead of pulsing linearly. Override--cui-spinner-animation-timing-functionto change either. -
.spinner-rotateis new. It applies only the rotation, so you can spin your own SVG or icon without inheriting the bordered circle. -
.btn-loadingsizes its spinner from the button.LoadingButtonno longer adds a-smclass to the spinner it injects; the spinner follows the button’s font size, so it now scales with.btn-smand.btn-lg.
Footer
-
Breaking
.footeris gone. Every declaration it carried is reachable with utilities, so<footer class="footer">becomes<footer class="d-flex flex-wrap align-items-center justify-content-between bg-3 border-top px-5 py-3">. The only property without a utility ismin-height: 3rem; where a floor matters, set it yourself or pad withpy-4. The--cui-footer-*variables and the$footer-*Sass variables go with it. -
Breaking
.footer-stickyis replaced by.sticky-bottom. It pins to the bottom the same way but sits on--cui-z-stickyrather than--cui-z-fixed, so a footer that has to stack above fixed chrome needs its ownz-index. -
Breaking
.footer-fixedis replaced by.fixed-bottom. It pins to the bottom of the viewport the same way and, unlike the footer class, is available in both editions.
Header
-
Breaking
.header-stickyis replaced by.sticky-top. The two rules were identical —position: sticky,top: 0,--cui-z-sticky— so the utility pins the header exactly as the component class did, and it composes with the responsive variants (.sticky-md-top) the component class never had. -
.header-fixednever existed. Thepositionproperty on the framework components acceptedfixedand rendered a class the stylesheet did not define, so it did nothing. Use.fixed-topor.fixed-bottom; on the framework components,positionrenders those utilities now. -
.prevent-hideon.header-toggleris removed. It forceddisplay: block !importantagainst a hide rule that no longer exists; nothing in the library or the admin templates sets the class.
Callout
- The callout takes its colour from a
.theme-*class, on the callout or on any element above it, like the alert does. The class colours the leading edge and tints the border, and because it exposes every slot,.theme-subtlenext to it fills the callout without naming the colour twice. - Deprecated
.callout-{color}still works and is removed in v7. Each modifier is generated from$theme-colorsthrough the sametheme-tokens()mixin as the theme class, so.callout-primaryis.callout.theme-primary. - Breaking
$callout-variantsis gone. The modifier classes are generated from$theme-colors, so adding a theme colour adds its callout class — there is no separate map to keep in step. - A callout without a colour no longer draws its leading edge in
currentColor.--cui-callout-border-left-colorwas read but never declared: only the modifiers set it, so the declaration was invalid on a bare.calloutand the property fell back to its initial value. It now defaults to the callout’s own border colour. - The 1px border of a coloured callout is tinted. It reads
--cui-theme-borderwhere it used to stay neutral, so a coloured callout is a coherent block rather than a grey box with a coloured tick. The leading edge is unchanged.
The deprecated colour classes are a separate stylesheet
.alert-{color},.btn-{color},.btn-outline-{color},.btn-subtle-{color},.btn-ghost-{color},.callout-{color},.chip-{color}and.list-group-item-{color}ship incoreui-legacy.css. Load it aftercoreui.cssand they render exactly as before; leave it out once your markup uses the theme classes. They are removed outright in v7.- The buttons carry the colour spelling in two places, not one. Besides the
loop that points each class at a theme colour, a variant’s base declarations
are repeated for the colour classes — the legacy sheet carries both, so
.btn-primarymoves whole rather than surviving as a rule with no colour. alert-modifiersandbtn-variant-loopseach covered two loops, one deprecated and one not. They are split, so the docs can mark the deprecated half without mislabelling the other: the alert variants loop isalert-variant-loop, and the deprecated button classes arebtn-deprecated-modifiers.
Helpers
-
.link-{color}is gone; a theme class colours a link. Reboot resolves a link’s colour from--cui-theme-fg, which every.theme-*class sets, so the colour follows the theme instead of needing its own class:<!-- v5 --> <a href="#" class="link-primary underline-opacity-25">Primary link</a> <!-- v6 --> <a href="#" class="theme-primary underline underline-opacity-25">Primary link</a>Two things change beyond the class name. The theme slot is a tone picked for text rather than the raw brand colour — it darkens on a light background and lightens on a dark one, where
.link-*gave the same colour in both, so the link is more readable in dark mode and slightly different in light. And the underline needs.underlineto carry it; its colour now defaults tocurrentColor, so it follows the theme unless you name one with.underline-{color}..link-body-emphasisbecomes.text-body-emphasis.The removed classes also computed their
:hovercolour in Sass, which froze it at build time — overriding--cui-primaryat runtime never reached the hover state. The theme slots are tokens, so it does. -
.ratiois now a native aspect-ratio utility. The helper built its ratio from a::beforewith a percentagepadding-top, which required a wrapper and absolutely positioned its only child. The class names are unchanged —.ratio-1x1,.ratio-4x3,.ratio-16x9,.ratio-21x9— but they now set the CSSaspect-ratioproperty on the element itself.<!-- v5 --> <div class="ratio ratio-16x9"> <iframe src="..."></iframe> </div> <!-- v6 --> <iframe class="w-100 ratio-16x9" src="..."></iframe>The wrapper and the bare
.ratioclass are gone; put the ratio class on the element you want sized. A custom ratio moves from a percentage to a ratio:--cui-aspect-ratio: 50%becomes--cui-ratio: 2 / 1, on.ratio-auto. The$aspect-ratiosmap moved fromhelpers/_ratio.scssto_config.scssand its values are ratios rather than percentages.
Sass architecture & design tokens
v6 adopts the Bootstrap v6 SCSS architecture: CSS cascade layers, per-component design-token maps, one shared styling surface for every text control, and runtime color math in place of precompiled shades. The global Sass knobs stay the build-time customization API, while a component’s own surface is its token map — several things below are breaking and are called out as such.
A theme class wins over a component’s own colour token
- Breaking
Under a
.theme-*class the theme wins over a component colour token. A component reads the--cui-theme-*slot on the property and its own token only as the fallback. In v5 the colour came from the component’s tokens, which a modifier such as.alert-dangerset, so a token you set on a coloured alert applied. In v6 a token set on a themed element, or for the whole page in your stylesheet or a$…-tokensmap, changes only the components without a theme; the alert, avatar, badge, callout, chip, checkbox, radio, switch, List Box indicator, sidebar and tooltip follow the same order as the card, toast, list group and nav. To recolour one themed element, set the slot rather than the component token:<div class="alert theme-danger" style="--cui-theme-bg-subtle: …">. - Variant classes keep the component token as the override point.
.btn-solidand the other button variants,.alert-solid,.badge-subtle,.badge-outline,.avatar-subtleand.chip-outlinemap the component tokens to their own slots and paint from the tokens, so on those elements a component token you set wins over the theme, as on a v5 button. Validation states (.is-invalid,.is-valid) win over a theme class too.
A numeric radius scale replaces the one global radius
- Breaking
Components read stops of a radius scale, and most got rounder.
:rootemits--cui-radius($radius,.5remby default) and the stops--cui-radius-0through--cui-radius-9ascalc()multiples of it, plus--cui-radius-pill, and component tokens read stops instead of the one global--cui-border-radius: buttons, inputs, list groups, tabs, nav links, progress, tooltips, thumbnails, callouts, combobox options and the range slider’s tooltip sit on--cui-radius-5(.5rem), alerts, badges, popovers, dialogs, drawers, menus, accordions, toasts, dropdowns, popups and cards on--cui-radius-7(.75rem), and nav pills on a fixed3rem, which keeps a pill up to 96px tall fully round and does not follow--cui-radius. Everything that used the.375remdefault is visibly rounder. To keep a component on its old radius, set its own token (--cui-card-border-radius: .375rem); to retune the whole page, set--cui-radius(every stop follows) or override single stops. The$border-radius(-sm/-lg/-xl/-xxl/-pill)variables and their--cui-border-radius-*tokens are gone with it: no component read them any more (the modal, the chip, the switch, the chip input, the search button’s key, the sidebar nav, the header toggler, the password-strength segments and the validation tooltip moved to the stop with the same value:--cui-border-radius→--cui-radius-4,-sm→-3,-lg→-5,-pill→--cui-radius-pill), and the.rounded-*utilities now read the scale too (see the utilities section).--cui-border-radiusis gone from:rootas well: nothing in the library read it, so a stylesheet setting it changed nothing — set--cui-radiusor a stop instead.
Interaction surfaces sit two rungs lighter on the ladder
- Breaking
Hover, active and disabled surfaces read lighter ladder rungs. Hover
states read
--cui-bg-1(was--cui-bg-3), active and disabled states--cui-bg-2(was--cui-bg-4), across list groups, menus, navs, pagination, breadcrumb links, the accordion’s open header, the file-input button and disabled controls; the badge’s neutral fill, the popover header, the progress track, the avatar fallback and input-group addons step down the same way. Interaction feedback is a tint rather than a block of gray — the same relationships, two rungs closer to the page. Set the component’s own token to keep a darker surface. - Breaking
The neutral button steps down the ladder with everything else. A
.btnwithout a theme colour rests on--cui-bg-2(was--cui-bg-4) and hovers on--cui-bg-3(was a hard-codedgray-300/gray-700pair outside the ladder); the neutral subtle variant rests on--cui-bg-2and the ghost variant’s hover wash is--cui-bg-1. The default button is visibly lighter. Its corners also now read--cui-btn-input-border-radius(--cui-radius-5,.5rem) instead of the global--cui-border-radius, so buttons match the fields they sit next to in input groups;.btn-groupclips to the same stop. Set$btn-bg/$btn-hover-bgor the--cui-btn-*tokens to keep the old look. - Breaking
The rest of the library steps down the same two rungs. The first pass
left a group of components on the old surfaces; they now read the rungs
their neighbours read. Hover surfaces on
--cui-bg-1: the dropdown link, combobox options and the select-all row (Autocomplete, Multi Select), the control-group action button and pagination focus (was--cui-bg-4, three rungs above its own hover). The calendar cell and the time-picker roll cell, which share the date-time picker’s panel, hover one rung higher on--cui-bg-2. Rest and selected surfaces on--cui-bg-2: the active nav link and nav tab (was--cui-bg-4), the selected combobox option, the neutral chip (hovering on--cui-bg-3instead of a mix with the foreground), the password-strength track and the search button’s key cap (active on--cui-bg-3). The range slider track sits halfway between--cui-bg-2and--cui-bg-3like the native range. The card header and footer read--cui-bg-1instead of a 3 % foreground wash, so a card on a tinted parent keeps a neutral cap. The tab’s hover border reads--cui-border-subtlerather than a surface rung. - Foreground roles line up too. The disabled calendar cell and the
disabled combobox option read
--cui-fg-4/--cui-fg-3like every other disabled control, the weekday header and the date-input placeholder read--cui-fg-3like every other caption and placeholder, and the active breadcrumb reads--cui-fg-1, one rung above the hovered link instead of the same one. - Breaking
Border tokens move one rung towards the page.
--cui-border-colorislight-dark(gray-200, gray-700)(wasgray-300/gray-800): lighter in the light scheme, and in the dark scheme one rung further from the body, where the old value sat too close to--cui-bg-bodyto read as a line.--cui-border-subtleis a half step, mixed fromgray-100andgray-200(gray-800/gray-900in dark) — plaingray-100had become invisible against--cui-bg-1on the spread-out light band.--cui-border-emphasizedisgray-600in the dark scheme (wasgray-700, one rung from the plain border). Everything that reads the tokens — cards, list groups, tables, inputs, tabs, the switch and check frames — follows.
The neutral ladder is three maps
-
Breaking
$theme-bgs,$theme-fgsand$theme-bordersreplace the neutral scalars.$fg-body,$fg-1…$fg-4,$bg-body,$bg-1…$bg-4,$border-subtle,$border-mutedand$border-emphasizedare gone; each map lives inscss/_theme.scssin thedefaults()shape,:rootemits every entry as--cui-bg-{key},--cui-fg-{key}or--cui-border-{key}, and the.bg-*,.fg-*and.border-*utilities are generated from the same maps.$border-colorstays a scalar — it is the default every component reads, not a rung. Override an entry instead of a variable:- $bg-body: #000; - $fg-body: #111; + $theme-bgs: ("body": #000); + $theme-fgs: ("body": #111);The maps also carry
white,black,bg/fg(the opposite ladder’s body) and, for backgrounds,transparent, so.bg-fg,.fg-bg,.fg-white,.fg-black,.border-bg,.border-whiteand.border-blackread tokens like every other rung..border-bodyreads--cui-border-body,light-dark(gray-300, gray-800), a step heavier than--cui-border-color; it was an alias of the default border before.
Links and rules read the ladder
--cui-link-colorislight-dark(var(--cui-primary-base), var(--cui-primary-fg))(was the base in both schemes), so a link on the dark page reads the lighter foreground stop instead of the fill colour, and--cui-link-hover-coloris a 10 % darkening of the link colour (color-mix(in oklch, var(--cui-link-color) 90%, var(--cui-black))) instead of--cui-primary-fg; override--cui-link-colorand the hover follows.--cui-hr-border-colorisvar(--cui-border-color)(wascurrentcolor): a rule is the same line as every other border, not a faint copy of the text colour.
Theme colours have no bare token
-
Breaking
--cui-primary,--cui-secondary,--cui-success,--cui-info,--cui-warning,--cui-dangerand--cui-inverseare gone. A theme colour is its set of slots —-base,-bg,-fg,-fg-emphasis,-bg-subtle,-bg-muted,-border,-focus-ring,-contrast— and the colour itself is the-baseslot:--cui-primary-baseis what--cui-primarywas, the 100–900 scale is mixed from it, and-bg(the action background) points at it. Rename reads one to one, and set the palette stop to retint a colour:- .brand { color: var(--cui-primary); } + .brand { color: var(--cui-primary-base); } - :root { --cui-primary: #6f42c1; } + :root { --cui-primary-500: #6f42c1; }Components that filled with the bare colour read
-basenow; the derived hover and active states of a.theme-*class are computed from-bg.
Breakpoints: lg 1024, xl 1280, 2xl 1536
- Breaking
The three largest breakpoints move and the largest is renamed.
lgis1024px(was 992),xl1280px(was 1200) and the top tier is2xlat1536px(wasxxlat 1400);smandmdstay. Every responsive class spells the tier the new way —.col-xxl-6is.col-2xl-6,.container-xxl.container-2xl,.modal-fullscreen-xxl-down.modal-fullscreen-2xl-down,.hstack-xxl.hstack-2xl,.drawer-xxl.drawer-2xl,.offcanvas-xxl.offcanvas-2xl— and the containers grow with them:.containeris1200pxatxl(was 1140) and1440pxat2xl(was 1320). Only.icon-xxlkeeps the spelling: it is a size, not a breakpoint. The Sass map is$breakpoints(was$grid-breakpoints) and the mixins take it under that name;$container-max-widthskeeps its name. The responsive placement of menus, tooltips and popovers (data-coreui-placement="lg:end") reads the same values, and the sidebar’s mobile mode,$mobile-breakpoint: lg, now starts below1024px— a tablet in landscape gets the mobile layout.
Fixed widths and a vertical gutter knob
- New:
.w-1….w-12, one$spacerper step from the$sizesmap, merged into the width utility next to the percentages. - Breaking
$grid-gutter-widthis$grid-gutter-x, and$grid-gutter-yjoins it.make-row()takes both (make-row($gutter-x, $gutter-y)) and writes them to--cui-gutter-x/--cui-gutter-y; the row gutter defaults to0as before, so nothing moves unless you set it.make-grid-columns()and the card group read$grid-gutter-x.$grid-gutter-widthstays as a deprecated alias of$grid-gutter-xuntil v7 — set the new name, the alias only reads.
The z-axis names the new components
- New keys in
$zindex-levels:menu,drawer,dialog, on the same values asdropdown,offcanvasandmodal(1000, 1045, 1055). Each emits a--cui-z-*token and a.z-*utility, and the menu, drawer and dialog read their own key instead of the legacy component’s. The legacy keys stay; both spellings of a pair always resolve to one number.
Every type stop carries its own line height
- Breaking
$font-sizessets a line height per stop instead of one value for all headings.xsis 1.25,sm…lgthe body leading (--cui-body-line-height, 1.5),xl2.5⁄1.75,2xl3⁄2.25,3xl1.2,4xland5xl1.1,6xl1 — the larger the type, the tighter the leading, and the small stops read at body leading instead of 1.2. The sizes are unchanged. Headings follow their stop through--cui-line-height-*:h5(lg) andh4(xl) get taller lines,h1(4xl) a tighter one.$headings-line-heightstill feeds--cui-heading-line-heightfor anything that reads it directly.
Layered shadows
- Breaking
Every shadow is a stack of two to four layers with a negative spread
instead of one wide blur: a tight layer for contact, wider ones for
ambient light, each at 4–9 % alpha, so a card reads as lifted rather than
smeared.
.shadow-xsand--cui-box-shadow-xsare new (one thin layer). The inset shadow reads 8 % in the light scheme and 26 % in the dark one.--cui-shadow-strengthin the dark scheme is2.4(was 2.5). The knobs are unchanged:--cui-shadow-colorand--cui-shadow-strengthon:root,.shadow-{color}and.shadow-opacity-*on the element,$shadowsin Sass — a custom entry is now a comma-separated list of layers.
A denser spacing scale
-
Breaking
$spacershas thirteen steps,0–12. From1the scale runs.25 / .375 / .5 / .75 / 1 / 1.25 / 1.5 / 1.75 / 2 / 2.25 / 2.5 / 3 rem(v5:.25 / .5 / 1 / 1.5 / 3), so every spacing, gap, space and gutter class changes value under the same number. The v5 steps map to:v5 step value v6 step 1.25rem12.5rem331rem541.5rem753rem12.p-3becomes.p-5,.mb-4becomes.mb-7, and the same for every margin, padding, gap, space and gutter class and its responsive variants. The components are unchanged: their tokens read the stops that carry the v5 values, so alerts, modals, drawers, popovers, callouts, list groups, the sidebar and the reboot margins keep their size. Cards are the exception, see Card.
Padding tokens carry both axes
- Breaking
A token that sets
paddingnames an axis. The six that v5 also had as custom properties are-padding-yand-padding-xnow:--cui-modal-padding,--cui-modal-header-padding,--cui-calendar-nav-padding,--cui-nav-enclosed-padding,--cui-date-picker-footer-padding,--cui-time-picker-footer-paddingand--cui-card-img-overlay-padding. The same goes for the ones v6 introduced, whether they are new or were Sass variables before (--cui-btn-close-padding,--cui-dialog-padding,--cui-dialog-header-padding,--cui-dialog-footer-padding,--cui-form-field-card-padding,--cui-list-box-section-label-padding,--cui-mark-padding,--cui-thumbnail-padding,--cui-date-picker-time-body-padding). Defaults are unchanged, except the card overlay’s (see Card), so set both to the old value to keep the same box. A single-value token could not be overridden per axis, and one of them was already a two-value shorthand (--cui-list-box-section-label-padding: .5rem .625rem), which made the horizontal padding unreachable without restating the vertical one. - Two tokens keep one value, because neither sets a
padding.--cui-switch-paddingis the inset between the track and the thumb: the thumb’s size subtracts it on both axes and only the inline offset reads it directly, so a vertical half would be a knob with nothing to set.--cui-list-inline-paddingis the space between.list-inline-items and sets a margin. - Breaking
--cui-modal-header-paddingis gone. It only restated--cui-modal-header-padding-y --cui-modal-header-padding-x, and setting it had no effect on the close button, which reads the two axes. The footer no longer reads the modal’s body padding either: it has--cui-modal-footer-padding-yand-padding-x, defaulting to--cui-modal-padding-y/-padding-x. - A distance between children is a
-gaptoken.--cui-date-picker-ranges-gap,--cui-date-picker-footer-gap,--cui-time-picker-footer-gap,--cui-drawer-body-gapand--cui-drawer-footer-gapare new; the time picker’s footer used to space its buttons with a.btn + .btnmargin, which only worked for buttons and could not be set from outside. The date picker’s ranges column and its time row also stopped borrowing the footer’s padding and border: they read--cui-date-picker-ranges-*and--cui-date-picker-timepickers-*.
Link decoration is a token, not a Sass knob
- Breaking
$link-decoration,$link-underline-offsetand$link-hover-decorationare gone. The underline lives on:rootas--cui-link-decoration(underline) and--cui-link-underline-offset(.2em); set the token instead of recompiling.--cui-link-hover-decorationis no longer declared — the hover falls back to--cui-link-decoration, so declare it yourself for a different hover. Nav links, list-group actions, page links, the header, the dropdown item and the button writetext-decoration: noneoutright (they used to skip it when the knob was alreadynone), and.btn-linkreads the tokens.
Every theme colour gets an action-background slot
:root emits --cui-{color}-bg for every theme colour, pointing at the
colour’s base token. Active and checked states read the slot instead of the
base directly — --cui-list-group-active-bg, --cui-menu-item-active-bg,
--cui-pagination-active-bg, --cui-nav-pills-link-active-bg,
--cui-progress-bar-bg and --cui-range-thumb-bg are all
var(--cui-primary-bg) now. The resolved values are unchanged; the slot is a
single place to retune what “filled with the theme colour” means without
touching the base colour that text and borders derive from. (The secondary
colour’s slot takes over the name of the former surface alias — see the
ladder entry above.)
- A
.theme-*class reads the colour’s-bgslot, not its base.--cui-theme-bgisvar(--cui-danger-bg)now (wasvar(--cui-danger)), so overriding--cui-danger-bgretunes every checked, active and filled state under the class, as the colour slots page describes. The trade-off:--cui-danger-bgresolves on:root, so a--cui-dangeroverride on a single element no longer reaches the theme class on it — override the colour on:root, or set--cui-danger-bgon the element.
Colour decisions are written, not computed
v6 stops evaluating colours at build time, the way upstream v6 does — Sass assembles maps of CSS expressions and the browser resolves them.
- The
contrastslot holds written values instead ofcolor-contrast()results. The computed and written values are identical, so nothing renders differently; the point is that a theme colour no longer has to be a Sass colour for the contrast pass to run over it. Upstream keeps a"contrast"key per colour and never calls the function either. - Table variants read the theme tokens.
Breaking
.table-primaryand friends still exist, but they are generated the way.btn-primaryis: the class sets--cui-theme-*and the base.tablerules read it.$table-variantsis now a list of colour names, not a map of pre-shifted colours, and$table-bg-scale/$table-border-factorare gone. A theme colour you add yourself now gets a table variant without compiling Sass. Two visible consequences: the variant background is the colour’sbg-subtletoken rather than ashift-color()of the base, and striped / hover / active arecolor-mix()overlays on the text colour rather than opaque mixes — so they compose over the row’s own background. Table variants also follow colour modes now, where before they stayed on light-palette values in dark mode.
The theme colours are retuned so white text reads on them
Breaking
Four of the five base colours moved, and success, info and danger now
carry white contrast text instead of black. The bases were picked at the
brightest point where white text still clears both contrast gates: WCAG 4.5:1
and APCA Lc 75, the APCA threshold for body text. Before this, only primary
paired with white, because the bases sat at lightnesses between 61 and 81 that
no dark-on-light pairing and no light-on-dark pairing could both satisfy.
| Colour | Before | After |
|---|---|---|
$primary | oklch(52.945% 0.19103 278.34) | unchanged |
$success | oklch(61.237% 0.17298 146.95) | oklch(55.1% 0.173 145) |
$info | oklch(67.55% 0.17646 252.26) | oklch(57.4% 0.196 256) |
$warning | oklch(80.791% 0.16473 78.5) | oklch(84% 0.172 85) |
$danger | oklch(63.864% 0.18175 23.81) | oklch(58.7% 0.237 23.81) |
success and info get darker, danger gains chroma, and warning goes the
other way: it is the one colour that cannot take white text at any hue, so it
stays on black and moved brighter, where black reads at Lc 75 instead of 69.
info also shifts four degrees of hue, which sounds cosmetic and is not: at
252 degrees the sRGB gamut caps chroma at 0.171 and the colour reads as petrol,
while at 256 the cap rises to 0.196 and it reads as blue.
Every colour is a 500 stop, so the whole scale moves with it and any component
tinted from these colours changes tone. If you pinned a base, nothing changes
for you. To keep the old look, set the five variables back through
@use ... with(); to keep the old contrast text, set "contrast" on the colour
in $theme-colors.
Two pairings are still open and are tracked rather than fixed here. Dark-mode
fg reads from the 400 stop and lands at Lc 42, well under the gate, and
fg-emphasis on bg-muted in light mode tops out at Lc 74 because a 200
background is too dark for any text to clear 75 against it.
RGB channel tokens and the elevation system removed
Breaking-
Every
--cui-{name}-rgbcustom property is gone, along with the$theme-colors-rgband$theme-colors-rgb-darkmaps, theto-rgb()function’s role in:root, and thergba-css-var()andtranslucent-css-var()helpers (color-var-ref()is the function that stays — see the utilities section). They existed only sorgba(var(--cui-primary-rgb), .5)could apply an alpha to a token; that is whatcolor-mix()does directly:- background-color: rgba(var(--cui-primary-rgb), .5); + background-color: color-mix(in srgb, var(--cui-primary) 50%, transparent);Upstream v6 carries no rgb triplets either. The opacity utilities (
.text-opacity-50,.bg-opacity-25,.link-opacity-75) are unchanged — they now scale the token throughcalc(var(--cui-bg-opacity) * 100%). -
The elevation system is gone:
$elevations,$enable-elevations,$elevation-base-color, theelevation()mixin and the.elevation-*utilities. It was disabled by default ($enable-elevations: false), so the utilities were the only reachable part. Use the shadow utilities (.shadow,.shadow-sm,.shadow-lg) or setbox-shadowdirectly. -
The button focus ring is the theme ring.
--cui-btn-focus-shadow-rgband the per-variant$btn-*-focus-shadow-rgbSass variables are gone without a renamed successor, and so is--cui-btn-focus-ring: the button draws the ring from the global width and the theme’s colour, so a colour button rings in its colour and a neutral one gets the global ring. To restyle it, set--cui-focus-ring-color(or--cui-theme-focus-ring) on the button. Two visible consequences: a colour button’s ring uses the focus-ring opacity (fainter than the old 50% mix), and the neutral outline/ghost buttons ring in the global colour instead of a grey.
Focus rings are keyboard-only on activation controls
BreakingControls you click to act — checkboxes, radios, switches, both range sliders,
pagination links, stepper buttons, the search, close, sidebar and navbar
togglers — draw their focus ring on :focus-visible, so clicking no longer
flashes it and keyboard focus still shows it, joining the buttons which
already worked this way. Fields you type into — text inputs, the OTP cells,
the date-time sections — keep :focus: a field shows its ring on mouse focus
by design.
Two suppressed rings are restored: the header toggler and the carousel
controls declared outline: 0 with no replacement, so keyboard focus was
invisible on both.
A validation state is a theme colour, not a palette of its own
Breaking$form-validation-states carried a colour, a border colour, a focus ring and a
pair of tooltip colours per state, and _root.scss emitted four tokens to feed
them. A state names a theme colour now, and every value is a slot of it.
$form-validation-states: (
"valid": (
- "color": var(--cui-form-valid-color),
- "border-color": var(--cui-form-valid-border-color),
- "tooltip-bg-color": var(--cui-success),
- "icon": …,
- …
+ "theme": "success",
),
)--cui-form-valid-color,-valid-border-color,-invalid-colorand-invalid-border-colorare removed, together with the Sass variables behind them ($form-valid-*,$form-invalid-*,$form-feedback-valid-color,$form-feedback-invalid-color). Retint a state by retinting its theme colour, or override the slot on the map.- Feedback text and the invalid border are one step darker, because both
read the state colour’s
fgslot instead of the raw brand colour. That is a fix, not a preference: measured against the body background, the old pair scored 3.50 (valid) and 3.69 (invalid), both under WCAG AA; the new pair scores 5.13 and 5.39. Text and border stay the same colour, as they were before. $input-btn-focus-color-opacityis removed. The validation ring comes from the colour’sfocus-ringslot, which reads--cui-focus-ring-opacity; set that instead.$focus-ringis removed and$focus-ring-colorreads the slot. The colour wasrgba($primary, $focus-ring-opacity), resolved at build time, so neither retinting--cui-primarynor changing--cui-focus-ring-opacityat runtime moved the default ring. It isvar(--cui-primary-focus-ring)now — the slot every.theme-*ring already mixed from.$focus-ringitself only restated--cui-focus-ring, which_root.scsscomposes, so the two variables that read it —$btn-close-focus-shadowand$sidebar-toggler-focus-shadow— are gone with it; the close button and the sidebar toggler draw their ring with the sharedfocus-ring()mixin.- Breaking
--cui-sidebar-toggler-focus-shadowis removed. It fed abox-shadowon.sidebar-toggler:focus-visiblenext to anoutline: 0, and its value was the--cui-focus-ringshorthand —<width> solid <color>, whichbox-shadowdoes not accept. The declaration was dropped as invalid and the toggler had no focus indicator at all. The ring is anoutlinenow, like everywhere else; restyle it through--cui-focus-ring-width,--cui-focus-ring-colorand--cui-focus-ring-offset. - Breaking
add()andsubtract()are removed. They wrappedcalc()to keep a unitless0from making the expression invalid. v6 computes derived values in the browser, so the stylesheet reaches forcalc()directly — 243 times, and not once for these two. Write thecalc()yourself; guard a possible0withif($value == 0, …)where it can actually occur.divide()stays, the grid still uses it. - Breaking
The theme colour tokens finish the rename to the slot names.
--cui-{color}-text-emphasisis--cui-{color}-fg-emphasisand--cui-{color}-border-subtleis--cui-{color}-border. The theme slots (--cui-theme-fg-emphasis,--cui-theme-border) already carried these names; the colour tokens they read did not, so the two halves of the same system spelled the same idea differently.--cui-{color}-baseis added alongside the bare--cui-{color}(itself gone since, the literal sits in--cui-{color}-500). The Sass keys move with them:"text-emphasis"→"fg-emphasis"and"border-subtle"→"border"in$theme-colors;$theme-colors-textand$theme-colors-border-subtleare gone with the other per-slot maps, andtheme-color-refs("fg-emphasis")returns the slot’s token for every colour. No class names change —.text-primary-emphasisand.border-primary-subtletake their suffix from the utility definition, not from the token. form-validation-state()takes$themewhere it took$color, and the variables moved fromscss/_config.scssinto the partials that use them alongside$form-text-*and$form-feedback-*.- The validation tooltip is opaque and sits on the state’s
-bgslot. It wascolor-mix(in srgb, var(--cui-danger) 90%, transparent), with the percentage frozen at build time from$form-feedback-tooltip-opacity. It readsvar(--cui-danger-bg)(-success-bg, or your state’s-bg) now, the same slot the alert and the badge use, so it follows a theme override with them.$form-feedback-tooltip-opacityis removed; for a translucent tooltip setbackground-coloron.invalid-tooltip/.valid-tooltip.
$component-active-color and $component-active-bg are removed
Breaking
One shared pair in _config.scss fed every active state in the library — the
list group, pagination, the menu, nav pills, the dropdown, both range sliders
and the checkable controls. Each component names its own default now and points
it at the colour’s slots, so nothing is defined in a file that never renders it.
- $list-group-active-color: $component-active-color;
- $list-group-active-bg: $component-active-bg;
+ $list-group-active-color: var(--cui-primary-contrast);
+ $list-group-active-bg: var(--cui-primary);- Active text is fully opaque.
$component-active-colorwasrgba($white, .87), a colour frozen at build time, so an active state recoloured to a light background kept near-white text. It reads thecontrastslot now. Against the default primary that lifts the ratio from 4.71 to 5.65, and it matches.text-bg-primary, which already used the slot — the two whites in the library are one white again. - Override the colour, not the pair. Set
$primary(or--cui-primary-500at runtime) to move every active state at once, or the component’s own knob to move one. Per instance,.theme-*still wins where a component reads--cui-theme-*. - The switch has no SVG knob at all any more. The thumb is painted with
background-colorin every state, so$switch-thumb-bgand$switch-checked-thumb-bgare ordinary colour knobs; the v5 knob images and the variables that fed them ($form-switch-bg-imageand friends) are gone.
A theme colour is one nested map entry
Breaking$theme-colors carried one value per colour and three further families carried
the rest, so a theme colour was defined in four places and adding one meant
editing four blocks. It is a map of maps now — one name, eight slots.
-
Twenty-four Sass variables are removed:
$primary-text-emphasisand its seven siblings,$primary-bg-subtleand its seven,$primary-border-subtleand its seven. Override a slot through the map instead:- $success-bg-subtle: #e8f5ec; + @use "@coreui/coreui/scss/coreui" with ( + $theme-colors: ( + "success": ( + "base": var(--cui-success-500), + "bg-subtle": #e8f5ec, + // …the remaining slots of the built-in entry + ), + ) + );The map merges one level deep, so a built-in colour is replaced as a whole: copy its sub-map from
scss/_theme.scssand change the slot.$theme-colorsgoes throughdefaults()like every other configuration map, so a new colour is one entry passed throughwith ()andnullremoves one.Every
--cui-{color}-*custom property keeps its name and its value, so a runtime override is unaffected. -
$theme-colorsis a map of maps, so@each $name, $value in $theme-colorsnow yields a map. Usemap-get-nested($theme-colors, "base")where a flatname → valuemap is what you want, ortheme-color-refs("base")for the:roottokens the utilities read. -
New slots:
fg(the state colour readable as text, one step lighter thantext-emphasis),bg-muted(betweenbg-subtleand the solid colour) andfocus-ring(the hue the.theme-*ring mixes from). They emit--cui-{color}-fg,-bg-mutedand-focus-ring, and reach components as--cui-theme-fg,--cui-theme-bg-mutedand--cui-theme-focus-ring; the emphasis text reaches them as--cui-theme-fg-emphasis.
The OTP input defaults to the shared frame
Twenty-five $form-otp-control-* variables were one-to-one aliases of the
$input-* family — $form-otp-control-bg: $input-bg and so on down to the
focus ring. They now default to the --cui-btn-input-* tokens on :root
instead, so the cell picks up a runtime restyle of the text controls without
going through a second Sass tier. The variables themselves stay, and so does
every rendered value.
One consequence for Sass customization: setting $input-bg (or another
$input-* knob) no longer reaches the OTP cell, because the cell now reads the
root token rather than that variable. Restyle the whole family through the
--cui-btn-input-* tokens, or set the matching $form-otp-control-* variable
alongside it.
The OTP cell is a .form-control frame
.form-otp-control now declares the whole $form-control-tokens map and
renders through the same form-control-frame() mixin as .form-control, so
a change to the text-control frame reaches the cell without a second copy.
The nineteen $form-otp-control-* variables that only re-pointed a
--cui-btn-input-* token ($form-otp-control-bg, -font-size,
-focus-ring and the rest) are gone with it; what remains is what the cell
sets differently — $form-otp-control-width, $form-otp-control-padding-x
and the size trios. Rendered values are unchanged. To restyle one cell set
the --cui-control-* token on it; to restyle the family, the
--cui-btn-input-* token on :root.
The input frame is complete on :root
The shared --cui-btn-input-* frame gains the slots every field was
still deriving at Sass level: border-color, border-radius (with -sm/-lg),
box-shadow, font-weight and the focus-color/-bg/-border-color trio.
The $input-* knobs keep working — they read the tokens now — so a with()
override reaches the whole family as before, and a runtime override of one
root token restyles every text control at once.
-
Breaking
$input-focus-border-coloris avar()reference, not a Sass colour. It wastint-color($primary, 50%), frozen at build time — retint the palette and the focus border stayed on the old primary. It reads--cui-btn-input-focus-border-colornow (the same 50% mix, computed in the browser). Sass colour functions on it no longer compile; override the token or the variable with a colour of your own instead. -
Breaking
$time-picker-roll-cell-selected-hover-bgis computed in the browser. It was alight-dark()pair ofcolor.scale($primary, …), frozen at build time, so a retinted palette moved the selected cell but left its hover state on the old indigo. It darkens the primary the way the solid button darkens its own background —oklch(from var(--cui-primary) calc(l * .85) c h)— which also drops the separate dark-mode variant, since the formula works in both. Sass colour functions on it no longer compile.
The field transition is three tokens, not one shorthand
Breaking--cui-control-transition held a whole shorthand
(border-color .15s ease-in-out, box-shadow .15s ease-in-out), so retiming a
field meant restating the property list. It splits into
--cui-control-transition-property, -duration and -timing, and the rules
set the three transition-* longhands. $input-transition splits the same way,
and so do the components that derived from it — $chip-input-transition,
$search-button-transition, $form-otp-control-transition,
$accordion-transition, $btn-transition.
The floating label follows the same rule: --cui-form-floating-transition and
$form-floating-transition are replaced by
--cui-form-floating-transition-property, -duration, -timing (and the
matching Sass variables). So does the range thumb — $form-range-thumb-transition
becomes $form-range-thumb-transition-property, -duration and -timing.
--cui-btn-input-transition is gone with them. The frame carries the parts
instead — --cui-btn-input-transition-duration, -timing, -property (what a
plain field animates: border-color, box-shadow) and
--cui-btn-input-action-transition-property (what a button or an action part
animates: the same plus color and background-color). One declaration retimes
every control on the page:
:root { --cui-btn-input-transition-duration: .3s; }One easing changes with the split: the accordion animated border-radius on
ease while everything else used ease-in-out; the longhands take a single
easing, so it is ease-in-out throughout.
The split also removes a trap the shorthand carries: a trailing duration binds
to the last property only, so adding color to the front of the old list would
have left it at 0s. The rendered animation is unchanged.
$form-check-* is now named for what it configures
Breaking
The checkbox, the radio and the switch shared their defaults through variables
named after .form-check — the v5 wrapper they no longer use. The names said
“deprecated wrapper” while the role was “the three current controls”:
| v5 | v6 |
|---|---|
$form-check-input-width | $control-size |
$form-check-input-focus-border | $control-focus-border |
$form-check-input-active-filter | $control-active-filter |
$form-check-input-disabled-opacity | $control-disabled-opacity |
$form-check-label-disabled-opacity | $control-label-disabled-opacity |
$form-check-input-checked-bg-color | $control-checked-bg |
$form-check-input-checked-border-color | $control-checked-border-color |
$form-check-input-indeterminate-bg-color | $control-indeterminate-bg |
$form-check-input-indeterminate-border-color | $control-indeterminate-border-color |
All nine sit in _root.scss and are emitted as --cui-control-* on :root,
so the three controls can also be retuned together without recompiling:
:root { --cui-control-disabled-opacity: .3; }$form-check-input-bg, -border, -border-radius, -checked-color and
$form-check-radio-border-radius are gone without a replacement: they were one
control’s value reached through another control’s name. Set $check-*,
$radio-* or $switch-* — the variables of the control you mean.
The five that shape the deprecated .form-check wrapper keep their names and
move into forms/_form-check.scss, so they leave the build with it.
The class names are unchanged. .form-check, .form-check-input and
.form-check-label still work exactly as before.
The $input-* variables are gone; configure the frame once
Breaking
$input-padding-y, $input-color, $input-bg, $input-border-radius and the
30 others were aliases: each one held nothing but var(--cui-btn-input-…), the
token the frame already declares on :root. Components read them in some places
and wrote the token out in others, so a Sass override reached part of the
library and not the rest — $input-color: lime recoloured the file-input button
and the input group addon but not the input’s own text.
There is one place to configure the frame now. $input-btn-* in _root.scss
holds the values, and every component reads the --cui-btn-input-* token it is
emitted as:
// before
@use "@coreui/coreui-pro" with ($input-padding-y: 1rem);
// after — Sass
@use "@coreui/coreui-pro" with ($input-btn-padding-y: 1rem);/* after — runtime, no recompile */
:root { --cui-btn-input-padding-y: 1rem; }$input-height-inner, -half and -quarter are geometry rather than aliases,
so they follow the validation mixin they serve into
scss/mixins/_form-validation.scss;
$input-height-border had one reader and is written where it is used.
The control variables moved out of the shared config
Breaking$input-* described one thing — the frame every field component draws — but sat
in _config.scss next to the breakpoints and the spacing scale. It lives in
scss/forms/_form-control.scss now, with .form-control itself.
- Nothing changes for CSS. The compiled stylesheet is identical; the
--cui-control-*and--cui-btn-input-*tokens are untouched, so runtime customisation is unaffected. - A Sass override needs the new path when you
@usethe partial directly. Overriding through@use "@coreui/coreui-pro" with (...)is unchanged. $input-btn-*stays in the config: those are the values the button and the field agree on, andbuttons/_button.scssreads them.$input-height,$input-height-smand$input-height-lgare removed. They existed to be baked into--cui-btn-input-min-height, which now computes from--cui-btn-input-line-height,-padding-yand-border-widthin the browser. Set those tokens, or--cui-btn-input-min-heightitself, instead of recompiling.
Shadows are computed at runtime
BreakingA shadow’s alpha is now base * strength * opacity, computed on the element
rather than resolved when the stylesheet is built. That is what makes a shadow
tintable and lets dark mode deepen every shadow at once.
-
$box-shadow,$box-shadow-sm,$box-shadow-lgand$box-shadow-insetare replaced by the$shadowsmap. The--cui-box-shadow*custom properties are unchanged in name and in rendered value — they are generated from the map — so runtime overrides keep working. Only the Sass override point moved:- $box-shadow-sm: 0 .125rem .25rem rgba($black, .1); + $shadows: map.merge($shadows, (sm: 0 .125rem .25rem rgba($black, .1)));A literal value like that opts the entry out of the runtime knobs, which is the point of overriding it.
-
New:
$shadow-color,$shadow-strength,$shadow-strength-darkand the--cui-shadow-color/--cui-shadow-strengthtokens they emit. Setting the colour once at the root recolours every shadow in the page, components included. -
New utilities:
.shadow-{color}for the theme colours pluscurrent/black/white, and.shadow-opacity-{25|50|75|100}. They write--cui-shadow-tintand--cui-shadow-opacity, both registeredinherits: false, so a tinted element does not recolour the shadows nested inside it. -
Dark mode raises
--cui-shadow-strengthto 2.5 instead of carrying a second set of shadow values. The same alpha reads as weaker against a dark surface, which is why the light-mode values looked almost absent there.
Theme gradients removed
Breaking.bg-{color}-gradientand.text-bg-{color}-gradientare gone, along with the$theme-gradients/$theme-gradients-darkmaps that generated them, the per-colour$primary-gradient…$dark-gradientvariables (and their-darktwins), and the--cui-{color}-start/--cui-{color}-stopcustom properties they read. These were CoreUI-only additions with no Bootstrap counterpart. Replace a removed class with the base colour plus the generic gradient —class="bg-primary bg-gradient"— or write the two-stop gradient yourself if you need the exact 45° ramp..bg-gradientitself stays, as do$gradient,--cui-gradient,$enable-gradientsand thegradient-bg()mixin — that is the gradient system Bootstrap has, and components keep using it.
Cascade layers
-
All library CSS now lives in cascade layers, declared as
@layer colors, config, root, reboot, layout, content, forms, components, custom, helpers, utilities;. The palette ships incolors, so your own@layer colorsblock overrides a token like--cui-blue-500by source order alone. Two of the layers are empty and reserved for you:config, belowroot, for a configuration sheet the library should read but never fight, andcustom, between components and helpers. -
Unlayered CSS wins over layered CSS regardless of specificity, so your existing stylesheets now override the library more easily, not less. No action needed — selectors that worked before keep working, and low-specificity overrides that used to lose on specificity now win.
-
Utilities no longer carry
!important. Breaking The$enable-important-utilitiesswitch is gone with the suffix, because the layer order above already puts every utility past every component — the suffix was doing nothing our own stylesheet needed. That takes 556 bytes gzip (19 kB uncompressed) off the build.The one case it did cover is your own unlayered CSS: since unlayered styles beat every layer, a plain
.card-title { text-align: left }in a stylesheet loaded after ours now wins over.text-center. Two ways out, and the choice is yours per rule rather than ours for all of them — move that CSS into@layer customso utilities win. -
If your build concatenates CoreUI with other CSS, make sure the layer declaration (emitted at the top of the bundle) is not stripped by a minifier that predates
@layer.
Design-token maps
-
Every component that emits CSS custom properties now builds them from a
$<component>-tokensSass map merged over your overrides with the newdefaults()function. Override the whole surface at once:@use "@coreui/coreui-pro/scss/coreui" with ( $alert-tokens: ( --cui-alert-bg: hotpink, --cui-alert-margin-bottom: null, // null removes the token entirely ) ); -
Breaking 941 Sass variables are gone, across 64 partials. Every scalar whose only job was carrying a value into a token map now sits in the map, so a value has one home instead of two. Set the token instead of the variable —
$popover-tokens: (--cui-popover-bg: hotpink)at build time, or--cui-popover-bgon the element at runtime. Moving a value out of a variable and into the map does not change what compiles out, so this reaches you only if your build set one of the removed variables, and Sass then names it:This variable was not declared with !default in the @used module.Each component page says which variables it still has: 31 lost their### Sass variablessection entirely. -
Breaking
scss/_root.scssdeclares no variables of its own any more — 62 went, leaving only$root-tokensfor overriding the map. The page-wide knobs that lived there are now the tokens they always emitted:$input-btn-*→--cui-btn-input-*,$control-*→--cui-control-*,$focus-ring-*→--cui-focus-ring-*,$backdrop-*→--cui-backdrop-*,$font-family-sans-serif→--cui-font-sans-serif,$line-height-base→--cui-body-line-height. Override them through$root-tokenswhen you compile, or on:rootat runtime:@use "@coreui/coreui-pro/scss/coreui" with ( $root-tokens: ( --cui-body-line-height: 1.6, --cui-btn-input-padding-y: .5rem, ) ); -
Breaking Sixteen more page-wide knobs are gone.
scss/_theme.scssdeclares no variables at all now: the four that set how far a hover or active shade travels are the tokens they fed,--cui-theme-hover-lightness,--cui-theme-hover-chroma,--cui-theme-active-lightnessand--cui-theme-active-chroma, and the theme page documents them. Fromscss/_config.scss,$gradient,$border-style,$font-family-base,$headings-colorand$link-colorgo the same way, to--cui-gradient,--cui-border-style,--cui-body-font-family,--cui-heading-colorand--cui-link-color. All seven$font-weight-*scalars are folded into$font-weights, which stays the way to add or drop a weight.dtand the headings read--cui-font-weight-boldand--cui-font-weight-mediuminstead of a number frozen at build time, so retuning a weight on:rootnow moves them too.$headings-line-heightgoes to--cui-heading-line-height,$font-size-baseto--cui-body-font-size, and$font-size-rootgoes entirely: it gated afont-sizeon the root element at build time and defaulted tonull, so it emitted nothing. Setfont-sizeonhtmlin your own CSS to scalereminstead.$border-widthand$border-colorgo the same way, to--cui-border-widthand--cui-border-color; the width scale and the.border-{key}classes read the token, not the variable.$blueand$pinkare finally gone from the source, which is what the palette entry above already said. -
Breaking The reboot declares no variables of its own, and 89 more scalars go with the same rule across the content partials, the legacy layer, the form label, text and file blocks, the callout, card, list group, dropdown and navbar dark blocks and the password strength meter. Each was a value on its way into a token map.
<th>keeps the browser’s own weight rather than a knob that emitted nothing,<caption>reads--cui-spacer-3and--cui-fg-3, andcodereads--cui-font-mono. -
markcarries its own tokens.--cui-mark-padding-y/-padding-x,--cui-mark-colorand--cui-mark-bgwere declared on:root, where nothing else read them. They sit onmark, .marknow, in a$reboot-mark-tokensmap you can override like any other component surface. Setting them on:rootno longer reaches the element; set them on the element, or on the map when you compile. -
Breaking 76 more component scalars are gone, the ones whose value reached exactly one custom property and nothing else: the chip and chip input size variants, the nav enclosed set, the OTP input sizes, the badge and rating sizes, the modal widths and margins, the toast surface, the form control group icon sizes and the colour picker width. Each is the token it fed.
-
Breaking The artwork variables are gone too. The accordion chevron, the breadcrumb divider, the four carousel controls, the close button glyph, the check and radio marks, the rating star, the select indicator and the navbar, header and sidebar togglers were each a Sass variable feeding one custom property. Set the property:
--cui-accordion-header-icon,--cui-breadcrumb-divider,--cui-carousel-control-prev-icon,--cui-btn-close-icon,--cui-check-mark-checked,--cui-radio-mark,--cui-rating-item-icon,--cui-control-select-bg-img,--cui-navbar-toggler-icon-bg,--cui-header-toggler-icon-bg,--cui-sidebar-toggler-icon. -
Breaking The validation icon inside the field is gone, and with it
$enable-validation-icons, the two glyphs and their colours, the geometry that made room for them and the select’s second background layer. A valid or invalid field still says so through its border, its focus ring and its feedback text. The glyph was abackground-imageon the input itself with the colour baked into the SVG, so it could never follow a token: an input renders no pseudo-element to hang a mask on, and masking the input erases its own background. It comes back when the field has a wrapper to draw it in. -
Breaking
$radiusand$chip-gapare gone, to--cui-radiusand--cui-chip-gap. The radius scale still multiplies one value, but that value is the token: set--cui-radiusand every stop follows at runtime. -
$font-weight-semiboldand$grid-gutter-widthare gone too. Both were kept only because the shared docs engine read them; it reads--cui-font-weight-semiboldand$grid-gutter-xnow. The weight lives in$font-weights, and$grid-gutter-xis the gutter. -
What stays in
scss/_config.scssstays for a reason.$spacer, the$enable-*switches, the breakpoint, container, spacer and border-width maps and the contrast helpers feed maps, mixins, loops and build-time decisions rather than one token, so they stay Sass.$theme-colorsstays for the same reason, inscss/_theme.scss. So does every component variable a rule or a mixin still reads. -
Component Sass variables moved from
_config.scssinto their component partials.@use ... with()is unaffected — the entry point forwards every module — but if you importedscss/_config.scssdirectly for its definitions, component blocks are no longer there. -
Two silent duplicates were dropped along the way:
$nav-link-heightwas declared twice (navbar and header blocks — the header copy is gone) and the calendar emitted--cui-calendar-cell-week-number-colortwice. -
Time picker: the v1-only tokens are gone. The rewritten shell draws its adornments as inline SVG on
currentColorand styles them through.form-control-action/.form-control-cleaner, so nothing read them any more —--cui-time-picker-cleaner-*(5 tokens),--cui-time-picker-indicator-icon,--cui-time-picker-indicator-icon-size,--cui-time-picker-gapand--cui-time-picker-placeholder-color, together with their Sass variables ($time-picker-cleaner-icon-size-sm/-lgwere also declared twice).--cui-time-picker-indicator-icon-colorstays and now carries the validation color as well. The validation state itself moved off the root element: v1 recoloured the frame from.time-picker.is-invalidwith!importantoverrides, v2 reads it from the field inside the group, so$time-picker-invalid-border-color/$time-picker-valid-border-colorand the matching indicator-icon variables are gone — theme the state through the standard$form-invalid-*/$form-valid-*variables instead. Section placeholders come from the field’s ownhourPlaceholder/minutePlaceholder/secondPlaceholderoptions, not a colour token. -
$input-padding-x-lgwas removed:.form-control-lgreads--cui-btn-input-lg-padding-xfrom the shared button/input token system, and the time picker’s deadgap-lgwas the variable’s last consumer.$input-padding-x,$input-padding-x-smand$input-padding-y-*are unaffected.
Theme tokens
- New
--cui-theme-*indirection plus generated.theme-primary….theme-inverseclasses: a themed container sets--cui-theme-fg,--cui-theme-bg-subtle,--cui-theme-border, and token-driven components (alert, buttons) read them. Contextual classes like.alert-successare now implemented through these tokens — same rendering, new mechanics. --cui-theme-focus-ringjoins the set. It carries the state colour mixed at$focus-ring-opacity— a colour, like every other slot; the width stays global. Every component draws its ring asvar(--cui-focus-ring-width) solid var(--cui-theme-focus-ring, var(--cui-focus-ring-color)), so a themed ancestor recolours the ring and the global colour applies everywhere else.--cui-{color}-contrastcustom properties are new root tokens. Previously only buttons computed their own contrast colour; the root tokens now carry it, written in thecontrastslot..text-bg-*reads the token instead of callingcolor-contrast().
--cui-mobile-breakpoint removed
Breaking
The token carried the name of the sidebar’s breakpoint (lg), not a length,
so it could not be used in a media query or in calc(), and nothing in the
library read it. Read --cui-breakpoint-lg for the value.
It also sat one letter away from a mechanism that does work and looks similar.
The sidebar tells its plugin about the mobile layout with --cui-is-mobile,
which is declared inside the media query, so it is "true" below the
breakpoint and empty above — the plugin only tests whether it is set. The
removed token was declared on :root unconditionally, so the same
Boolean(getPropertyValue(…)) test against it answered “mobile” at every
width.
The Sass variable $mobile-breakpoint is unchanged; the sidebar’s own media
queries are built on it.
Colour scales, and _variables-dark.scss removed
Breaking
Dark mode no longer has a second set of Sass variables. Every colour is declared
once, as a native
light-dark()
pair, and [data#{$data-infix}theme="dark"] only declares color-scheme: dark.
-
scss/_variables-dark.scssis gone. All 59$*-darkvariables it held are gone with it; do not@import "variables-dark"any more. The values live inline in the light variable:- $body-color: rgba(shift-color($gray-base, +26%), .95); - $body-color-dark: rgba($white, .87); // in _variables-dark.scss + $body-color: light-dark(rgba(shift-color($gray-base, +26%), .95), rgba($white, .87));One exception remains, because a colour baked into an SVG cannot read
light-dark(): the select chevron and the switch knob keep a dark counterpart next to their component ($form-select-indicator-dark,$form-switch-bg-image-dark). -
Every palette colour gets a 025–975 scale of tokens in
:root, mixed at runtime from its500stop:--cui-primary-025…--cui-primary-975, for each key of$colors. Knobs:$color-tints,$color-shades,$color-mix-space(oklch),$tint-color,$shade-color. Override--cui-primary-500at runtime and the whole scale retints. -
Breaking The palette is the theme colours.
$colorsholdsprimary,success,info,warning,dangerandgray;$blue,$indigo,$purple,$pink,$red,$orange,$yellow,$green,$teal,$cyanand every--cui-{hue}and--cui-{hue}-{stop}token are gone. Nothing read them: no utility was generated from them and the theme never pointed at them.$secondaryand$inverseare gone too — they read gray stops, not a colour of their own. Each$theme-colorsentry pointsbaseandbgat its palette’s500stop (primaryatvar(--cui-primary-500)) and the other slots at that scale;secondaryreads--cui-gray-200,inverse--cui-gray-900, and theirbgis thelight-dark()pair that used to be their base. No palette token is bare: the literal sits in the-500stop, so--cui-gray-500is what--cui-graywas.- :root { --cui-primary-base: #6f42c1; } + :root { --cui-primary-500: #6f42c1; } - .hint { color: var(--cui-teal-600); } + .hint { color: var(--cui-success-600); }A new theme colour is a colour in
$colorsplus an entry in$theme-colorsthat points at it; see adding a theme colour. -
Breaking The seven
$theme-colors-{slot}maps are removed ($theme-colors-fg,$theme-colors-fg-emphasis,$theme-colors-bg-subtle,$theme-colors-bg-muted,$theme-colors-border,$theme-colors-contrast,$theme-colors-focus-ring). They were flat copies of one slot each;theme-color-refs("bg-subtle")returns the same shape as token references, which is what every utility reads. -
Breaking The palette is written in
oklch()and mixes inoklch. The base colours are unchanged — every hex converts to the same sRGB pixel — but the 100–900 steps mixed from them shift, becauseoklchholds chroma wherelabdrained it (measured: 5.9 per RGB channel on average, 28 at most).$blackis#000now rather than a near-black with a blue cast:oklchinterpolates hue, so mixing a scale towards a tinted black rotated every shade towards blue. -
Breaking The gray ramp is generated like every other colour.
$gray-025…$gray-975were thirteen hand-picked values; they are now one$graybase run through the same generator, andgrayis an entry of$colors. The Sass variables stay, derived from the base, so$gray-600keeps working — but the ramp is set by$grayplus$color-tints/$color-shades, not by overriding a stop.$color-tintsand$color-shadesgained025,050,950and975, so every colour now carries the full thirteen stops. The curve is94 / 90 / 80 / 60 / 40 / 20 %of white for025…400and16 / 32 / 48 / 64 / 70 / 76 %of black for600…975, and$grayisoklch(60% 0.03948 265.02)— the v5 hue and chroma, a cool gray, at the lightness the curve is tuned for. The light band is far more spread out than the v5 ramp it replaces: against white,gray-100reads 1.27:1 (was 1.10),gray-2001.63 (was 1.21),gray-3002.14 (was 1.34),gray-4002.87 (was 1.49);gray-600and below are within a step of where they were. Everything that reads a light stop moves with it — the neutral surfacesbg-1…bg-4,border-color,theme-secondary, every-bg-subtleand-borderslot — which is what makes a plain badge or a secondary button visible on a white page again. -
-bg-subtle,-border-subtleand-text-emphasisare pairs of those stops rather than hand-picked hexes:$primary-bg-subtle: light-dark(var(--cui-primary-100), var(--cui-primary-900)); $primary-border-subtle: light-dark(var(--cui-primary-300), var(--cui-primary-600)); $primary-text-emphasis: light-dark(var(--cui-primary-800), var(--cui-primary-200));This fixes a real dark-mode bug. Only
lightanddarkhad genuine dark variants before; for the six chromatic colours$primary-bg-subtle-darkwas an alias of the light value, so.alert-success,.list-group-item-infoand.table-warningrendered a pale light-mode surface in dark mode. They now darken. Light mode shifts slightly too: subtle backgrounds are a shade lighter (an 80% tint rather than ~70%) and emphasis text a shade darker; borders are unchanged for practical purposes. Emphasis-on-subtle contrast improves in every colour — the weakest pair goes from 6.03:1 to 7.57:1. -
The dark grey palette is gone:
$gray-100-dark…$gray-900-darkand the$grays-darkmap. Every one of them was defined as exactly its light counterpart, so they generated nothing — read$gray-*directly. -
$theme-colors-darkis gone, along with$theme-colors-fg-emphasis-dark,$theme-colors-bg-subtle-dark,$theme-colors-border-darkand$theme-colors-contrast-dark, and the eight$primary-dark…$dark-darkvariables behind them. Theme colours themselves no longer change between colour modes — the old dark variants were a 10% desaturation, which upstream does not do either. Consequence: the eight[data#{$data-infix}theme="dark"] .text-bg-*overrides are gone. -
The
--cui-{name}-darkroot tokens are gone (--cui-body-color-dark,--cui-body-bg-dark,--cui-emphasis-color-dark,--cui-secondary-color-dark,--cui-secondary-bg-dark,--cui-tertiary-color-dark,--cui-tertiary-bg-dark). They existed so a component could pin the dark palette while the page stayed light; declarecolor-scheme: darkinstead and thelight-dark()pairs follow — which is exactly what.sidebar-darknow does, having dropped its eight token overrides. -
$link-shade-percentageno longer drives$link-hover-color, which is nowlight-dark(var(--cui-primary-600), var(--cui-primary-400)). The variable still exists and still drives the.link-*-*hover helpers. -
Two dead table knobs were removed:
$table-th-font-weightand$table-caption-color. Reboot owns those declarations, so use$th-font-weightand$caption-color. -
Setting
$enable-dark-mode: falsestill yields a light-only page::rootdeclarescolor-scheme: lightand nothing ever declaresdark, so everylight-dark()pair resolves to its light side.
The neutral surfaces are a numbered ladder
.bg-body-secondary, .bg-body-tertiary, .text-body-secondary and
.text-body-tertiary are deprecated. The replacements are ordinal:
.bg-1 … .bg-4 for surfaces and .fg-body, .fg-1 … .fg-4 for the text
hierarchy, with two new rungs (.bg-1, .bg-2 on new gray-025 / gray-050
stops) between the body and the old surfaces. The deprecated names render
identically and are removed in v7.
The adjectives ran backwards for backgrounds — “tertiary” sat closer to the page than “secondary” — because for text, where the names came from, “stronger” means more visible, while for a surface it means further from the page. Numbers say how far from the body a rung sits and read the same on both sides.
The deprecated surface names follow the two-rung step of the rest of the
library: .bg-body-secondary renders --cui-bg-3 and .bg-body-tertiary
--cui-bg-2 (gray-100 / gray-050 in the light scheme, the v5 stops one
rung apart), .text-body-secondary stays --cui-fg-2 and
.text-body-tertiary --cui-fg-3 — tertiary text was the lighter of the
two in v5 and still is.
Two consequences for the tokens:
- Deprecated
The surface tokens move out of the theme-color namespace.
--cui-bg-body,--cui-bg-1…--cui-bg-4and--cui-fg-body,--cui-fg-1…--cui-fg-4are canonical;--cui-body-bg,--cui-body-color,--cui-secondary-color(--cui-fg-2),--cui-tertiary-color(--cui-fg-3) and--cui-tertiary-bg(--cui-bg-2) remain as aliases with identical values, and go away in v7 — at which point the--cui-secondary-*prefix belongs to thesecondarytheme color alone. One alias is already gone: Breaking--cui-secondary-bgis thesecondarytheme color’s action-background slot now (see the entry below), not a surface alias. Markup that read it for the surface gets a similar but lighter gray — read--cui-bg-4instead. This name is exactly why the ladder moved out of the theme-color namespace: a page surface sat next to--cui-secondary-bg-subtle(a theme-color slot) with no way to tell which system a token belonged to. - Breaking
The dark side of the ladder is palette stops, and the page got darker.
The grays gain
--cui-gray-950(#15181f) and--cui-gray-975(#0e1115), and every dark rung is a stop instead of acolor-mix()betweengray-800andgray-900: the body sits ongray-975(wasgray-900),bg-1ongray-950,bg-2ongray-900,bg-3ongray-800andbg-4ongray-700. Dark mode reads darker overall and surfaces have more room to step away from the page.--cui-border-color-translucentis one expression for both schemes,color-mix(in oklch, var(--cui-fg-body) 15%, transparent), instead of tworgba()values computed in Sass; thecolor-translucent()function is gone. Override the$bg-*knobs or the--cui-bg-*tokens to keep the previous palette. - Breaking
The text ladder is solid grays, and components use all of its rungs.
--cui-fg-bodythrough--cui-fg-4were alpha blends of one color (rgba(…, .95)down to.25), which made the two deepest rungs illegible and left every component crowded ontofg-body/fg-2. Each rung is a palette stop now —fg-bodyongray-900/050,fg-1on800/200,fg-2on700/300,fg-3on600/500,fg-4on500/600— so contrast is the same on any surface and can be audited once. Components spread across the rungs: secondary text (breadcrumb links, toast headers, captions, blockquote footers) readsfg-3instead offg-2, disabled states readfg-3/fg-4and stay legible, dividers readfg-4, and the strongest text (tables, active nav links, the tooltip surface) readsfg-bodyinstead of--cui-emphasis-color. Nav links are neutral (fg-2, darkening throughfg-1tofg-bodywhen active) instead of link-colored — a.theme-*class still recolors them.$gray-basefed the alpha blends and is removed. Override the$fg-*knobs or the--cui-fg-*tokens to keep the previous rendering. .text-bodyand.text-body-emphasisare unaffected — they are not ordinal, any more than.text-primaryis.- Breaking
The Sass knobs follow the ladder. The
$theme-fgsand$theme-bgsmaps ("body","1"…"4") replace$body-color,$body-bg,$body-secondary-color,$body-secondary-bg,$body-tertiary-colorand$body-tertiary-bg; every entry holds onelight-dark()expression, so$body-bg-darkand$body-tertiary-bg-darkare gone too — the translucent tints that needed their Sass colors read the grays inline.
light and dark are replaced by inverse
Breaking
light and dark were theme colours whose names described a colour scheme, and
they behaved like it: .theme-dark sat next to data-coreui-theme="dark"
meaning something else entirely, and neither followed the reader’s scheme — a
.btn-dark stayed near-black on a near-black page. There are seven theme colours
now, and the two neutrals swap ends of the gray scale between schemes.
inversereplacesdark. It resolves togray-900in light mode andgray-100in dark, so.btn-inverse,.text-bg-inverseand.table-inversestand against the page in both.secondarytakes over whatlightdid. It is a light neutral now (gray-100in light mode,gray-600in dark) rather than the mid gray-blue#6b7785, so.btn-secondaryand.bg-secondarychange appearance.- Every
-lightand-darkclass is gone. Rename-darkto-inverseand-lightto-secondaryacrossalert,bg,border(and its per-side variants),btn,btn-ghost,btn-outline,btn-subtle,callout,chip,focus-ring,underline,list-group-item,shadow,table,text,text-bgandtheme, plus the-subtleand-emphasisfamilies. The colour-scheme opt-ins keep their names:.navbar-dark,.dropdown-menu-dark,.carousel-darkand.sidebar-darkare unaffected, as is.fw-light. - A surface is not a theme colour. Where
.bg-lightwas a neutral panel, reach for.bg-4or.bg-3; where.bg-darkwas a fixed dark backdrop, setdata-coreui-theme="dark"on the container and let the surface classes follow. --cui-theme-baseis now--cui-theme-bg. The contextual token that components read for a fill was named after the map slot rather than the job; the slot is stillbase, and--cui-{color}is unchanged.- Colour utilities read the slot that stays legible.
.text-*points at--cui-{color}-fginstead of the raw colour, and.btn-outline-*and.btn-ghost-*take their text from it too. For the five chromatic colours this is a shade darker in light mode and a shade lighter in dark; for the neutrals it is the difference between readable and gray-on-gray..btn-outline-*also takes its border from theborderslot, so the outline is a subtle line rather than a full-strength one..bg-*and.border-*keep reading the colour itself. $lightand$darkare removed;inverseis new. Neithersecondarynorinverseis a hue of its own: they read--cui-gray-200and--cui-gray-900, and there is no Sass colour behind them to pass tocolor-contrast()or the tint/shade functions — read$gray-*for that.$utilities-colorsis removed. It was a flatname → colourcopy of the map’sbaseslot, and no utility reads the raw colour for every family any more. Callmap-get-nested($theme-colors, "base")where you need one, ortheme-color-refs("fg")for the slot a text utility uses.$theme-lightness-overridesis now$theme-state-colorsand carries ready values rather than lightness multipliers. A single multiplier cannot serve a colour that is dark in one scheme and light in the other, so the two neutrals step along the gray scale instead.
Component icons render through CSS masks
BreakingThe accordion chevron and the carousel arrows were background-image SVGs with
the colour written inside the markup, which is why each needed a dark-mode rule —
the accordion swapped in a second SVG, the carousel inverted the first with a
filter. They are now painted through a CSS mask, so the colour lives on
background-color as an ordinary token and follows the root colours by itself.
Upstream v6 does the same, with a mask-icon() mixin.
- The accordion’s icon is one shape recoloured per state, not two images.
--cui-accordion-header-active-iconno longer has to carry a second copy of the artwork: it defaults to--cui-accordion-header-icon, and the open state changes--cui-accordion-header-active-icon-colorinstead. Set it only when the open state is a genuinely different shape — a minus against a plus. New tokens:--cui-accordion-header-icon-colorand--cui-accordion-header-active-icon-color. - A custom icon is now a shape, not a coloured image. Whatever colour you put
inside
$accordion-header-iconor$carousel-control-prev-icon-bg/$carousel-control-next-icon-bgis ignored — only the alpha of the SVG is used. Set the colour through the token (--cui-accordion-header-icon-color,--cui-carousel-control-color). $accordion-icon-colorand$accordion-icon-active-colorarecurrentcolornow, not Sass colours — Sass colour functions on them no longer work. The icon therefore takes the header’s colour in every state, so anything that recolours an open header (--cui-accordion-active-color, a.theme-{color}class) carries the icon with it. Both tokens stay, for the case where the icon is meant to differ from the text.$carousel-control-icon-filteris removed, with its--cui-carousel-control-icon-filtercustom property and its two dark companions ($carousel-control-icon-filter-dark,$carousel-dark-control-icon-filter), along with the four accordion dark variables ($accordion-icon-color-dark,$accordion-icon-active-color-dark,$accordion-button-icon-dark,$accordion-button-active-icon-dark). The accordion’s dark-mode block is gone entirely.- The carousel’s colours are tokens.
--cui-carousel-control-coloris new;.carousel-darkpins the dark side of each pair through$carousel-dark-control-color,$carousel-dark-indicator-active-bgand$carousel-dark-caption-color(previously$carousel-*-darkin_variables-dark.scss). RTL still swaps the two arrows, now throughmask-image. - Visible in dark mode: the old dark accordion chevron was a filled glyph where the light one was stroked. Both modes now use the stroked shape — same colour, same size, slightly different drawing. Worth knowing if you diff screenshots.
Buttons
-
Breaking
button-variant()takes a variant name, not a colour pair, and reads it from$button-variants. Each entry of that map names, per state, the theme token a button variable reads and the neutral value behind it;disabledrepeatsbase.button-outline-variant()andbutton-ghost-variant()are removed (they are entries in the map now), andscss/mixins/_buttons.scssno longer exists — the mixin lives next to the map inscss/buttons/_button.scss. -
Hover and active shades are computed in CSS with
oklch(from var(--cui-primary) calc(l * …) c h)instead of precompiledshade-color()/tint-color(). Shades shift slightly (measured 8–23 per RGB channel). The win: a theme color you add yourself gets correct hover/active states without compiling Sass. -
The eight shade/tint amount knobs are removed:
$btn-hover-bg-shade-amount,$btn-hover-bg-tint-amount,$btn-hover-border-shade-amount,$btn-hover-border-tint-amountand the four$btn-active-*counterparts. They fed the Sass mixes that no longer happen. -
Breaking The interaction shade belongs to the theme, not to the button. Every
.theme-*class emits--cui-theme-hoverand--cui-theme-active— its own colour, shifted in lightness — and the button variants read them, so a component that wants “this theme colour, one step darker” no longer has to recompute it. The knobs moved with them:$btn-hover-lightness,$btn-active-lightnessand$btn-lightness-overridesare now$theme-hover-lightness,$theme-active-lightnessand$theme-state-colorsinscss/_theme.scss. -
The interaction shade is a runtime knob, and it keeps its saturation. The multipliers are
:roottokens —--cui-theme-hover-lightness,--cui-theme-hover-chroma,--cui-theme-active-lightness,--cui-theme-active-chroma— so the strength of every hover and active state is one override, on the page or on a subtree, where it used to be a Sass value baked into each theme class. The shade also multiplies chroma (1.1 on hover, 1.15 on active) where it used to keep it, so a pressed button no longer fades as it darkens; the lightness steps are unchanged. -
The dark-mode button block is gone (~160 lines of CSS) — variants follow the root color tokens, which already switch in dark mode.
-
Breaking The
$btn-*variables that only forwarded a shared value are removed:$btn-color,$btn-padding-y,$btn-padding-x,$btn-font-family,$btn-font-size,$btn-line-height,$btn-border-width,$btn-border-radius,$btn-disabled-opacity,$btn-transition-*and the-sm/-lgpairs. They pointed at--cui-btn-input-*, so a value assigned to them was overwritten by the token anyway. Configure the frame once on:rootwith the$input-btn-*variables, or set--cui-btn-*on the button. -
Breaking
$btn-font-weightis removed, and buttons take their weight from the shared frame.--cui-btn-font-weightreads--cui-btn-input-font-weight, the weight fields and input group addons use, which defaults to--cui-body-font-weighton:root. A page that sets a lighter body weight gets lighter buttons with it;.btn-linkkeeps the normal weight. Set--cui-btn-font-weighton.btnto change buttons alone, or--cui-btn-input-font-weighton:rootto change buttons and fields together. -
Breaking
button-size()is removed. Sizes are a loop over$button-sizes, which points the button’s tokens at the matching--cui-btn-input-{size}-*set. Add a size by adding its tokens on:rootand its name to the map. -
New tokens on
.btn:--cui-btn-gap,--cui-btn-min-height,--cui-btn-white-spaceand--cui-btn-transition-property/-duration/-timing. The transition was read straight from the shared--cui-btn-input-*tokens before, so it could not be retuned per button. -
Breaking
.btn-closedraws its focus ring as anoutline, like every other control;$btn-close-focus-shadowand--cui-btn-close-focus-shadoware removed. A box-shadow ring was clipped by theoverflow: hiddenof the modal, toast or drawer the close button usually sits in. Its geometry is tokens now too:--cui-btn-close-width,-height,-padding-x,-padding-y. -
.btn-primary,.btn-outline-*,.btn-ghost-*class APIs are unchanged. -
Toggle buttons:
.btn-checkmoves onto the<label>, with the input nested inside. A toggle button was two elements held together by anid/forpair; it is one element now, so nothing can fall out of sync:- <input type="checkbox" class="btn-check" id="t" autocomplete="off"> - <label class="btn btn-primary" for="t">Toggle</label> + <label class="btn btn-primary btn-check"> + <input type="checkbox" autocomplete="off"> + Toggle + </label>The states read the nested input with
:has()instead of sibling selectors, and a.btn-groupstops having to reason about a hidden child that is not a button. The old spelling keeps working — the label carries.btnin the new one, so the two select themselves apart — and renders identically; it is deprecated in v6 and removed in v7. One behavior is deliberately carried over: a disabled toggle is dimmed rather than repainted, so a disabled checked toggle still reads as checked.
Utilities API: one property map replaces css-var, css-variable-name and local-vars
Breaking
Three keys said one thing between them — a utility may write custom
properties, real properties, or both — and property now says it alone. Give
it a map: a null entry receives the utility’s value, everything else is
written as declared.
"background-color": (
- property: background-color,
- local-vars: ("bg-opacity": 1),
+ property: (
+ --#{$prefix}bg: null,
+ --#{$prefix}bg-opacity: 1,
+ background-color: color-mix(in srgb, var(--#{$prefix}bg) calc(var(--#{$prefix}bg-opacity) * 100%), transparent),
+ ),
class: bg,
values: $utilities-bg-colors
)
"bg-opacity": (
- css-var: true,
+ property: (--#{$prefix}bg-opacity: null),
class: bg-opacity,
values: (25: .25, 50: .5, 75: .75)
)The class API does not change — .bg-primary and .bg-opacity-50 still exist
and still compose. What changes is where the translucency expression lives:
$utilities-text-colors,$utilities-bg-colors,$utilities-border-colorsand$utilities-links-underlinenow carry plain token references (var(--cui-primary),var(--cui-primary-fg)) instead of acolor-mix()expression per entry. The expression is written once, in the utility’spropertymap. If you built a map of your own by callingtranslucent-css-var(), callcolor-var-ref()instead — same arguments, without the wrapper;translucent-css-var()is removed, along with the unusedmap-loop()andcolor-contrast-variables()..text-body-secondarynow responds to.text-opacity-*. It rendered the same colour before and renders the same colour now; it simply went through the plain value and so ignored the opacity knob every other colour utility honoured.- Every custom property a utility assigns is registered
@property { inherits: false }, which now covers--cui-bg,--cui-text,--cui-borderand--cui-underlineas well — so a colour set on a container cannot leak into a descendant that only carries an opacity class.
at-property: false still opts a utility out of that registration, for the one
case that writes an inheriting global token (.focus-ring-*).
Utilities API: child selectors and attribute selectors (new)
Two keys join the $utilities API, ported from Bootstrap’s v6 branch:
child-selectormakes a utility style the children of the element it is applied to instead of the element itself, wrapped in:where()so it adds no specificity. This is what the new spacing and divider utilities are built on.selector(class,attr-starts,attr-includes) writes an attribute selector rather than a class, so one rule can serve a whole family of classes.
They bring four new utilities with them, all responsive and direction-aware:
| class | what it does |
|---|---|
.space-x-*, .space-y-* | margin between children — gap without needing a flex or grid container |
.divide-x, .divide-y (+ -0) | a border between children, none on the outer edges |
Nothing existing changes; these are additions. See the utilities API, spacing and borders.
Utilities API: theme variants and the rtl key removed
Breaking
- The
dark:theme-variant utilities are gone (40 classes:.dark\:bg-*,.dark\:text-*). They were generated for the colour utilities only, duplicating every value under a[data-coreui-theme=dark]selector, and were never documented. The palette already resolves per theme throughlight-dark(), so.bg-body,.text-body-secondaryand friends follow the colour mode on their own. Thedark-modekey of a$utilitiesentry is no longer read. - The
rtlkey of a$utilitiesentry is no longer read. It did two jobs:rtl: falsewrapped the output in RTLCSS/* rtl:remove */directives, andrtl: trueaccepted("ltr": …, "rtl": …)value maps that emitted a[dir="rtl"]counterpart. Direction is expressed with logical properties now (the position utilities areinset-inline-start/-end), so the flag has nothing left to do. One behaviour change comes with it: the RTL behaviour of.translate-middle/.translate-middle-xis written as a plain:dir(rtl)rule instead of a generator option —transformis not direction-aware, so centring on a logical inset still needs the flip, but it is the only utility that does and it no longer costs the whole API a key.
RFS removed
Breaking- The RFS engine is gone — no
vendor/rfs, nofont-size()/rfs()mixins, no$enable-rfs, norfsoption in the utilities API, and none of the$rfs-*knobs ($rfs-base-value,$rfs-breakpoint,$rfs-factor,$rfs-mode,$rfs-unit,$rfs-two-dimensional,$rfs-class, …). Fluid type is written as literalclamp()values in the$font-sizesmap,$display-font-sizesand$legend-font-size; rendering is unchanged at every viewport width. - Headings no longer derive from
$font-size-base— aclamp()cannot be multiplied. Override the stop in$font-sizesdirectly. - A temporary
scss/vendor/_rfs.scssshim (pass-throughfont-size()/rfs()mixins) exists only for the docs engine and will be removed — do not depend on it.
One styling surface for all text controls
- Shared root knobs:
:rootnow carries the control frame (--cui-btn-input-padding-y/-x,-font-family/-size,-line-height,-border-width,-transition,-color,-bg,-disabled-opacity, plus-sm-*/-lg-*size sets) and the selection state (--cui-control-checked-bg/-border-color,--cui-control-indeterminate-bg/-border-color). Restyle these once and every control follows: inputs, selects, checks, switches, OTP, chip input, autocomplete, multi select, and the pickers — including the checkbox-style option indicators inside autocomplete and multi select. - Every text control consumes per-element
--cui-control-*tokens (padding, font, colors, border, radius, shadows, focus and disabled sets) that default to those knobs. Override a single control on its element; override everything at:root. Breaking The component-namespace custom properties that duplicated this frame are renamed:--cui-autocomplete-padding-y,--cui-date-picker-font-size,--cui-time-picker-bg,--cui-form-multi-select-border-color,--cui-chip-input-padding-xand the rest of their frame sets are now--cui-control-*on the same elements. Runtime overrides written against the old names must be updated. Component-specific tokens (indicators, dropdowns, cells, cleaner buttons) keep their names. Three v5 names that survived as undeclared fallbacks are gone too:--cui-input-placeholder-color(set--cui-control-placeholder-color),--cui-input-focus-border-colorin the combobox (set--cui-btn-input-focus-border-color) and--cui-table-caption-color(the caption reads$caption-color). - Component Sass variables survive with new defaults:
$date-picker-bg,$autocomplete-padding-yand the rest now default to the shared knob (var(--cui-btn-input-bg)…). Untouched, the control follows the shared surface; overridden throughwith(), it detaches just that control — exactly what reaching for a component knob means. - The select’s indicator geometry is computed at runtime.
$form-select-indicator-paddingand$form-select-feedback-icon-padding-endare nowcalc()expressions overvar(--cui-control-padding-x)instead of Sass products, and$form-select-bg-positioncarries thevar()inside it (the mirrored keyword comes from the:dir(rtl)rule in the same sheet). Breaking Sass arithmetic on these variables in your own stylesheets no longer works — they are CSS calc strings now, not numbers. Override them with your owncalc()or a plain length instead. - Form tooltips reuse the tooltip’s runtime tokens. Add the
.tooltipclass to a feedback tooltip (class="tooltip invalid-tooltip") and it follows tooltip theming —--cui-tooltip-padding-*,-font-size,-border-radius— the upstream v6 contract. Bare.invalid-tooltipmarkup keeps rendering exactly as before through built-in fallbacks. Breaking The Sass-level derivation is gone:$form-feedback-tooltip-*no longer default to$tooltip-*, so overriding$tooltip-padding-yalone no longer moves feedback tooltips at compile time — use the runtime link (add the class) or set$form-feedback-tooltip-*explicitly. The tooltip variables moved toscss/_tooltip.scss, the feedback set toscss/mixins/_form-validation.scss. - Validation composes through the tokens:
.is-invalid/.is-validset--cui-control-border-color,-focus-border-colorand-focus-ringinstead of restating declarations, so state styling follows any control automatically.
The spacing scale is computed from --cui-spacer in the browser
- Breaking
--cui-spacer-1…-12arecalc()multiples of--cui-spacer, and the spacing utilities read the stops.$spacersused to multiply$spacerwhen the stylesheet was built and every.m-*,.p-*,.gap-*,.space-*and.g-*class carried the literal length, so the only way to rescale spacing was to recompile. Now:rootdeclares--cui-spacer($spacer, 1rem) and the stops ascalc(var(--cui-spacer) * n), the utilities writevar(--cui-spacer-n), the negative margins and the.w-*sizescalc(var(--cui-spacer) * n), and the gutters read the same stops.:root { --cui-spacer: .75rem }rescales all of it at run time, and overriding one stop re-spaces every utility on it. The scale moved next to the breakpoints inscss/layout/_tokens.scss, so the grid, utilities and reboot bundles each declare it; a$root-tokensoverride of a--cui-spacer-*key no longer reaches it — override the custom property instead.
The border-width scale is computed from --cui-border-width in the browser
- Breaking
--cui-border-width-1…-5and-keylinearecalc()multiples of--cui-border-width, and the.border-*width classes read them.$border-widthsshipped literal pixel values and every.border-2,.border-top-3,.border-start-5class carried the length, so changing--cui-border-widthre-weighted the components and left the utilities where they were. Now:rootdeclares the stops as multiples of--cui-border-width(1px):-keylineis half of it,-1the base,-2…-5its multiples, and the width classes writevar(--cui-border-width-n).:root { --cui-border-width: 2px }re-weights utilities and components together; overriding one stop re-weights every class on it.--cui-border-width-keylinekeeps its name and moves into the scale.
Spacing utilities write logical shorthands
.mx-*, .my-*, .px-* and .py-* (and their negative-margin variants)
emit margin-inline, margin-block, padding-inline and padding-block
instead of two physical longhands each. Rendering is identical in both
directions; the only visible difference is in DevTools, where one shorthand
replaces the pair, and in a stylesheet of your own that overrode one of the
longhands after ours — a shorthand still yields to a later longhand.
Opacity utilities no longer cascade to descendants
BreakingThe opacity utilities compose through custom properties — .text-opacity-50 sets
--cui-text-opacity and the colour utilities read it. Custom properties
inherit, so the value leaked into every descendant. The colour utilities that
restate their own opacity (.text-primary declares
--cui-text-opacity: 1) were immune; the ones reading it through a
var(…, 1) fallback were not.
Reachable consequences in v5: .bg-opacity-25 on a container faded every nested
.text-bg-*, and .link-opacity-50 faded every nested link — including bare
<a> elements, which read --cui-link-opacity too.
Five names are now registered with
@property as
inherits: false, so each utility is scoped to the element it is applied to:
--cui-text-opacity, --cui-bg-opacity,
--cui-border-opacity, --cui-link-opacity,
--cui-underline-opacity. Upstream v6 does the same.
- If you relied on the leak — one
.link-opacity-*on a wrapper fading a whole block of links — put the utility on each element, or set the custom property yourself in your own CSS (a hand-written declaration still inherits; only the registration changes how utilities behave). .focus-ring-*is deliberately not registered: it writes--cui-focus-ring-color, which is a global:roottoken, and components read it without a fallback. A custom utility that writes a:roottoken name must opt out the same way, withat-property: falsein its$utilitiesentry.- Compiled output is otherwise byte-identical — the five
@propertyblocks are the whole diff.
The CSS Grid ships, and its responsive classes are container queries
Breaking$enable-cssgrid is true by default, so .grid and the .g-col-* /
.g-start-* classes are in the compiled CSS instead of waiting behind a Sass
flag. Set it to false if you do not want them.
Every .grid is now a query container (container-type: inline-size), so
.g-col-md-6 responds to the width of the grid rather than to the viewport — a
grid inside a narrow column switches layout on that column’s width. This is
what upstream v6 does, and it is the point of having a second grid at all.
container-type implies layout containment, so a .grid is the containing block
for absolutely and fixed-positioned descendants: position: fixed inside a grid
is positioned against the grid, not the viewport. Our own floating panels render
through a portal and are unaffected; your own fixed markup has to live outside.
The CSS Grid declares its variables, and nested grids no longer inherit
Breaking.grid read --cui-rows, --cui-columns and --cui-gap through a var()
fallback and declared none of them. Two consequences: the variables did not
appear in dev tools, and — because a custom property that is never declared
simply inherits — a nested .grid picked up the column count of the grid
around it.
All three are declared on .grid now, so every grid starts from twelve
columns, one row and the standard gap regardless of its surroundings. A
component built on .grid keeps its own layout wherever it is dropped.
If you relied on a nested grid inheriting, set the variable on that grid too:
<div class="grid" style="--cui-columns: 3;">
<div>
<div class="grid" style="--cui-columns: 3;">...</div>
</div>
</div>The cq-* grid is gone
Breaking
v5.3.0 shipped a second, experimental grid — .cq-container, .cq-row,
.cq-col-*, .cq-offset-*, .cq-g-* — behind $enable-container-queries,
with its own $cq-grid-breakpoints map that duplicated $grid-breakpoints
value for value. It never shipped in the default build, and maintaining two
grids to express one idea is what the CSS Grid above now does properly.
The flag, the map, the classes and the make-c-grid-columns() mixin are all
removed. If you configured $enable-container-queries, drop it: @use ... with () errors on a variable the module no longer declares. If you had opted in: .cq-row/.cq-col-* map onto the CSS Grid
(.grid + .g-col-*), which is container-based by default, or onto the flexbox
grid (.row/.col-*) if you want viewport breakpoints. To keep the container
behaviour on markup of your own, mark the element with
.contains-inline and write the query with the
container-breakpoint-* mixins.
Breakpoint mixins emit range queries
Breakingmedia-breakpoint-* now emits (width >= 768px) / (width < 768px) instead of
(min-width: 768px) / (max-width: 767.98px). The .02px subtraction existed
to work around min-/max- prefixes on fractional viewports; range syntax makes
it unnecessary.
breakpoint-max() returns the raw breakpoint value now (768px, not
767.98px). If you call it yourself inside a max-width: query, subtract the
.02px there or move that query to range syntax too — otherwise the two
breakpoints overlap on exactly 768px.
The container-breakpoint-* mixins use the same syntax, take an optional
container name as their second argument, and default to $grid-breakpoints:
@include container-breakpoint-up(md) { … } // nearest container
@include container-breakpoint-up(md, sidebar) { … } // @container sidebar (width >= 768px)set-container($type, $name) is new and marks an element as a query container.
New: container-type utilities
.contains-inline and .contains-size set container-type, so you can make any
element a query container from markup. See
Container queries.
Sass file layout follows Bootstrap v6
BreakingUpstream’s directories mirror the cascade layers, so partials now live in the
directory named after the layer they emit into. The old paths no longer
resolve — point your @use rules at the paths on the right.
| Was | Is |
|---|---|
scss/_variables.scss | scss/_config.scss |
scss/_grid.scss, scss/_containers.scss | scss/layout/ |
scss/mixins/_breakpoints.scss | scss/layout/_breakpoints.scss |
scss/_reboot.scss, scss/_type.scss, scss/_images.scss, scss/_tables.scss | scss/content/ |
scss/_buttons.scss | scss/buttons/_button.scss |
scss/_button-group.scss, scss/_close.scss, scss/_loading-button.scss, scss/_search-button.scss | scss/buttons/ |
scss/_spinners.scss | scss/_spinner.scss |
scss/_placeholders.scss | scss/_placeholder.scss |
scss/_mixins.scss, scss/_helpers.scss, scss/_forms.scss | scss/mixins/index.scss, scss/helpers/index.scss, scss/forms/index.scss |
scss/forms/_form-check.scss | scss/forms/_check.scss |
scss/forms/_form-otp.scss | scss/forms/_otp-input.scss |
The list rules from _type.scss moved to content/_lists.scss and the blockquote
rules to content/_blockquote.scss, matching upstream’s filenames.
The three aggregators are unaffected: Sass resolves @use "mixins" to
mixins/index.scss on its own.
@import is no longer supported
Breaking
The 87 *.import.scss files that let @import reach our modules are gone, so an
@import of anything under scss/ no longer resolves. Load the library with
@use instead:
// Was
@import "../node_modules/@coreui/coreui/scss/coreui";
// Is
@use "@coreui/coreui/scss/coreui";Configuration moves with it. Where @import let you set variables by declaring
them before the import, @use takes them in a with () clause, and the rule has
to be the first thing in the file:
@use "@coreui/coreui/scss/coreui" with (
$body-bg: #000,
$body-color: #111
);Sass deprecated @import and removes it in Dart Sass 3.0.0, so this is where it
was heading regardless. The Sass migration guide
covers the rules; sass-migrator module automates most of a conversion.
One consequence worth calling out: a module is configured once, at load time. If
your change has to read a default before changing it — merging into
$utilities, for instance — it goes in a partial you load before the library.
The utilities API page shows both patterns.
What we deliberately did not copy. Upstream renamed three components and their
classes — .modal, .offcanvas, .dropdown, .dropdown-menu and
.dropdown-item are gone from Bootstrap v6, replaced by .dialog, .drawer,
.menu and .menu-item. Those are three of the most-used components in any
Bootstrap app, so _modal.scss, _offcanvas.scss and _dropdown.scss keep their
names and their classes here.
The colour system has its own modules
Breaking_config.scss no longer defines a single colour. The palette — the base
colours, the grays and their maps — lives in scss/_colors.scss; the theme
colour system — the $theme-colors map, the runtime
colour-scale knobs and the $theme-colors-* slot maps — lives in
scss/_theme.scss. scss/_maps.scss is dissolved: the $utilities-* maps and
$negative-spacers sit in scss/_utilities.scss next to the $utilities
definitions they feed, and $gutters in scss/_config.scss with the rest of
the grid settings.
- Nothing changes for CSS — the compiled stylesheet is byte-identical, and every custom property keeps its name.
- Overriding through
@use "@coreui/coreui-pro/scss/coreui" with (...)is unchanged — the entry point forwards all of these modules. scss/mapsis gone andscss/variablesis not part of the public surface any more. Load.../scss/config,.../scss/colors,.../scss/themeor.../scss/utilitiesdirectly, depending on what you need.
The type scale is one named map
One scale now carries every font size the library renders, each stop with the line height that belongs to it — upstream’s mechanic, with our values, so nothing moves on screen:
$font-sizes: (
"xs": ("font-size": .75rem, "line-height": 1.5),
...
"4xl": ("font-size": clamp(1.375rem, 1.375rem + 1.5vw, 2.5rem), "line-height": 1.2)
);-
$font-sizeschanged shape — it was(1: $h1-font-size, … 6: $h6-font-size)and is now keyedxs,sm,md,lg,xl,2xl,3xl,4xl,5xl,6xl, with a nested map per stop. The two top stops sit above the headings — display sizes ($font-size-5xl/-6xl, up to3remand3.5rem) continuing the same fluid progression, numerically level with the former.display-5and.display-4. Breaking If you overrode$font-sizes, move your values into the new shape. The numeric keys live on in$font-sizes-legacy, which is derived from the named scale. -
:rootgets--cui-font-size-*and--cui-line-height-*per stop, andh1–h6read the tokens instead of the Sass variables. Restyling a stop at runtime now moves every heading and utility that uses it.$h1-font-size…$h6-font-size,$font-size-sm/-lg,$font-size-5xl/-6xland$zindex-*are gone: each was read only by the map it fed, so the maps ($font-sizes,$zindex-levels) hold the values and are the override point, in thedefaults()shape — set one key and the rest stays.$font-weight-*remain and feed$font-weightsthe same way. -
New:
.fs-{stop}alongside.fs-1….fs-6, which keep working and render identically —.fs-1is.fs-4xl, down to.fs-6, which is.fs-md. -
New:
.text-{stop}, which setsfont-sizeandline-heighttogether. -
Breaking
.display-1….display-6and.leadare removed, with$display-font-sizes,$display-font-family,$display-font-style,$display-font-weight,$display-line-height,$lead-font-sizeand$lead-font-weight. They were a fixed size and weight next to a scale that already carries those sizes:.display-4is.fs-6xl,.display-5is.fs-5xl, and.leadis.fs-lg; add.fw-lightfor the weight the old classes carried and.lh-1for the display line height. The three larger display sizes have no stop of their own — setfont-sizeyourself or extend$font-sizes.
We did not follow upstream’s other typography changes. Bootstrap v6 also
deletes .fs-1 … .fs-6 and retunes the sizes — h1–h3 grow up to 10px on small
screens while h4–h6 shrink at every width. Those classes stay, at their current
sizes.
The focus ring is an outline
BreakingFocus rings were drawn with box-shadow and are now drawn with outline,
following upstream v6. The practical win is that an outline is not clipped by an
ancestor’s overflow — the ring stays visible inside scroll containers, where a
box-shadow ring was cut off.
-
The per-component ring custom properties are gone, not renamed. Every
--cui-*-focus-box-shadow— button, pagination, accordion button, check, radio, switch, calendar cell and nav button, stepper indicator, range-slider thumb, search button, combobox, chip — had exactly one value, and the ring is now drawn from the global width and the theme’s colour wherever it appears. Restyle it with--cui-focus-ring-color/--cui-focus-ring-widthfor the whole page, or set either on the component itself for one instance.- --cui-btn-focus-box-shadow: 0 0 0 .25rem rgba(88, 86, 214, .5); + --cui-focus-ring-color: rgba(88, 86, 214, .5);A runtime override written against an old name is now inert — there is no fallback path, because the value goes to a different property.
Form fields draw the same ring: the text input, select, control group, date-time field, OTP cell and List Box option read the theme’s focus ring or
--cui-focus-ring-color, and a validation state draws its own ring in its colour. The ring has no per-field colour token. -
The Sass variables follow:
$focus-ring-box-shadow→$focus-ring, and the per-component ones ($btn-focus-box-shadow,$pagination-focus-ring,$combobox-focus-ring,$form-check-input-focus-ring, …) are removed. -
$focus-ring-bluris gone, along with--cui-focus-ring-x,-yand-blur. An outline has no blur or offset vector. New:$focus-ring-offset/--cui-focus-ring-offset(defaults to0), which pushes the ring away from the element, and--cui-focus-ring, the composite shorthand the mixin reads. -
focus-ring()changed signature tofocus-ring($offset: false, $color: null). It no longer emitsoutline: 0. Every ring in the library is drawn through it; with no arguments it draws the themed ring, and$colorswaps the hue for one rule — which is how the validation states ring in red and green. -
--cui-focus-ring-offsetactually applies. The mixin emitsoutline-offsetwherever a ring is drawn, so one declaration moves every ring in or out. It was previously read by the.focus-ringhelper alone, which made the token look supported while doing nothing on any component. -
$pagination-focus-outlineis gone — the outline is now the ring itself. -
A component ring is a reference to the global one, not a copy of it. Every ring resolves as
var(--cui-focus-ring-width) solid var(--cui-theme-focus-ring, var(--cui-focus-ring-color)), so setting--cui-focus-ring-coloror--cui-focus-ring-widthat the root reaches every control, and a.theme-{color}region tints the rings inside it. Previously each component baked the resolved value in at build time, which is why a runtime change to the global ring left the components behind.- --cui-pagination-focus-ring: .25rem solid rgba(88, 86, 214, .25); + /* nothing to declare — the pagination link rings from the global tokens */ -
$input-btn-focus-colorno longer reaches the input and control rings. They read the global ring now; set$focus-ring-color(or--cui-focus-ring-color) instead.$input-btn-focus-widthis unaffected — it still sizes the validation rings and the button.
Sizes and colours are unchanged, so the ring keeps its width
($focus-ring-width, .25rem) and its translucent theme colour. What changes is
the property drawing it, which also means the ring no longer participates in a
box-shadow transition.
Everything deprecated before v6 is removed
BreakingSome of these carried a “to be removed in v6” note since v4.2. This is v6.
Sass mixins — all three were already unreachable, forwarded from nothing:
alert-variant(), list-group-item-variant(), avatar(). The contextual classes
they used to build are generated by a Sass loop over the theme tokens instead.
Sass variables — $variable-prefix (use $prefix), $border-radius,
$border-radius-sm, $border-radius-lg, $border-radius-xl, $border-radius-xxl,
$border-radius-2xl and $border-radius-pill (use the $radii map and $radius),
$text-muted, $hr-bg-color, $hr-height,
$alert-bg-scale, $alert-border-scale, $alert-color-scale,
$list-group-item-bg-scale, $list-group-item-color-scale,
$tooltip-arrow-color, $popover-arrow-color, $popover-arrow-outer-color,
$accordion-button-focus-border-color,
$dropdown-header-padding (use -padding-y / -padding-x),
$nested-kbd-font-weight, $btn-close-white-filter and $btn-close-filter-dark,
$high-emphasis, $medium-emphasis, $disabled, $high-emphasis-inverse,
$medium-emphasis-inverse, $disabled-inverse, $dropdown-inner-border-radius
(the first and last menu item now take that radius from
--cui-dropdown-border-radius, -border-width and -padding-y directly),
$calendar-cell-range-hover-bg (the range hover state draws dashed borders, not a
background), $body-text-align and $input-btn-font-family (both defaulted to
null, so the tokens they fed were never emitted and every font-family /
text-align that read them was invalid; buttons and controls inherit the body
font, set font-family on the element to change it).
Thirteen of those were already dead — nothing read them, so removing them changed no output at all.
CSS custom properties — --cui-high-emphasis,
--cui-medium-emphasis, --cui-disabled, and the three
-inverse counterparts, plus --cui-border-radius-sm, -lg, -xl, -xxl,
-2xl, -pill and --cui-border-radius itself (use --cui-radius,
--cui-radius-0 … -9 and --cui-radius-pill),
--cui-dropdown-inner-border-radius (the edge-item radius is computed from the
dropdown’s own tokens now) and --cui-calendar-cell-range-hover-bg (declared and
never read),
--cui-font-monospace (renamed --cui-font-mono), --cui-highlight-color and
--cui-highlight-bg (renamed --cui-mark-color / --cui-mark-bg),
--cui-btn-input-color (renamed --cui-btn-input-fg) and --cui-control-color
(renamed --cui-control-fg, on .form-control and everything sharing its
frame),
--cui-body-text-align, --cui-btn-input-font-family, --cui-btn-font-family,
--cui-control-font-family and --cui-search-button-font-family (never emitted,
see the Sass variables above). Optional knobs that stay null by default —
--cui-nav-link-font-size, --cui-nav-link-font-weight, --cui-modal-footer-bg,
--cui-form-feedback-font-style, --cui-form-feedback-tooltip-line-height — are
now read with a fallback (inherit / transparent), so the declaration is valid
until you set them.
Classes — eleven CoreUI-only additions, plus the three text utilities Bootstrap deprecated in 5.0 and removes in 6:
| Removed | Use instead |
|---|---|
.text-high-emphasis, .text-medium-emphasis | .text-body, .fg-2 |
.text-disabled | .fg-3 |
.text-high-emphasis-inverse, .text-medium-emphasis-inverse, .text-disabled-inverse | .text-white with an opacity utility |
.text-muted | .fg-2 |
.text-black-50, .text-white-50 | .fg-black.fg-50, .fg-white.fg-50 |
(The .dark: variants of the emphasis utilities go with them.) The
$navbar-light-* Sass variables stay.
What we deliberately kept. .sidebar-brand-full, .sidebar-brand-narrow
and .d-narrow-none stay, though they are CoreUI’s own. They are the brand pair
every admin template ships in its sidebar header, and they are exact aliases of
.d-sidebar-narrow-none / .d-sidebar-narrow — two extra selectors in a rule
that already exists. Removing them would break the most recognisable piece of
CoreUI markup, silently: both logos would render at once in the narrow state,
with nothing to indicate why. Prefer the .d-sidebar-narrow* names in new
markup.
Sass & CSS
-
The picker shell styling is a lean partial (
_date-picker.scss, ~110 lines / 16 CSS-var tokens vs v1’s 369 lines / 79 Sass variables per picker): the shell styles only the dropdown panel, the input-group frame, the footer/ranges layout, and the indicator. The field is styled by the date input partial, the popup content by the calendar, projected footer/ranges content is userland (style your own buttons). Sass overrides targeting the v1 input-layer classes (.date-picker-input,.date-picker-input-wrapper,.date-picker-input-preview,.date-picker-cleaner,.date-picker-separatoricon masks) will not apply to the v2 markup — those layers no longer exist. Overrides on the surviving classes (.date-picker-popup,.date-picker-footer,.date-picker-ranges) keep working — but the frame and the indicator now come from the shared primitive below. -
The frame of every field component is
.form-control-group. Autocomplete, chip input, multi select, password input and all four pickers each drew their own copy of it before; they now share one. What this changes for you:v5 v6 .date-picker-input-group,.time-picker-input-group,.autocomplete-input-group,.form-multi-select-input-group, the frame on.chip-input, the.form-passwordwrapper.form-control-group.date-picker-indicator,.time-picker-indicator,.autocomplete-indicator,.form-multi-select-indicator,.form-password-action.form-control-action.autocomplete-cleaner,.form-multi-select-cleaner.form-control-cleaner.autocomplete-inputplain .form-control— the group strips its frame, so the editable needs no class of its own.autocomplete-buttons,.form-multi-select-buttonsgone: the adornments are children of the group, which lays them out .form-multi-select-tag,.form-multi-select-tag-delete.chip,.chip-remove.date-picker-separator.form-control-icon.date-picker-sm/-lg,.time-picker-sm/-lg,.autocomplete-sm/-lg,.form-multi-select-sm/-lg.form-control-sm/-lgon the group (the components apply it fromsizethemselves) -
Multi Select renders every selection as a Chip.
selectionType: 'tags'and'chips'used to be the same idea built twice, the default'tags'being a private copy of the chip without the roles chips carry. Both values are still accepted and now produce the same markup, so a selection is a chip everywhere in the library — style it through the--cui-chip-*properties. The$form-multi-select-tag-*variables are gone with it.'chips'is the name and the default now;'tags'renders the same and is deprecated until v7. The selection container.form-multi-select-selection-tagsis.multi-select-selection-chips, with no alias. -
Multi Select’s classes drop the
form-prefix, so they carry the component’s name like.autocomplete,.chip-inputand.date-picker. There is no alias for styling:v5 v6 .form-multi-select.multi-select.form-multi-select-selection,.form-multi-select-selection-tags.multi-select-selection,.multi-select-selection-chips.form-multi-select-search,.form-multi-select-placeholder.multi-select-search,.multi-select-placeholder--cui-form-multi-select-selection-tags-gap,-padding-y--cui-multi-select-selection-chips-gap,-padding-yThe filled state is
.multi-select-filled, the Sass partial isscss/forms/_multi-select.scsswith$multi-select-tokens, and the component stylesheet iscss/components/forms/multi-select.css. -
Multi Select starts from
data-coreui-multi-selectonly. A v5<select class="form-multi-select">was initialized on page load by its class; in v6 it stays a plain native select. Replace the class with the attribute, as every other field component is started:- <select class="form-multi-select" multiple> + <select data-coreui-multi-select multiple> -
The per-family frame variables are gone.
$autocomplete-font-size,$date-picker-border-color,$time-picker-focus-bg,$form-multi-select-padding-yand the rest of each family’s copy of the input look mirrored the shared frame value for value — and since the frame declares the--cui-control-*properties on itself, an override through them never reached it. Theme the frame once through the$input-*variables, or per instance through--cui-control-*on.form-control-group. Each family keeps only the variables for what is genuinely its own (placeholder colours, the pickers’ footers, the chip row).Markup you write by hand has to be updated: the password input wrapper (
<div class="form-control-group">with a.form-control-actionbutton) and the chip input (<div class="form-control-group chip-input">). The components that build their own DOM do it for you.The adornment icons are inline SVG rendered by the component now, not CSS masks, so they take their colour from the button (
currentColor) and follow it through hover, validation and disabled — the per-family icon, icon colour and icon size variables are gone with them. Every adornment across the library is the same mark at the same size.Sass overrides move with the classes. Because the group is the control frame, theming
.form-control— or the--cui-control-*custom properties — now reaches all of these components at once, which is what the per-family variables were emulating. The families keep the variables for what they actually own (calendars, the time roll, listboxes, chips), and the ones that only aliased$input-*are gone. -
The floating panel of every field component is the
.popupprimitive. The pickers’ dropdowns and the combobox listbox drew the same box three times; the generated panel now carries.popup(alongside its structural class), and the chrome lives in one place. Sass-wise,$date-picker-dropdown-*,$time-picker-dropdown-*, the chrome half of$combobox-popup-*and the per-family-zindexvariables are replaced by$popup-zindex/-bg/-border-width/-border-color/-border-radius/-box-shadowand the--cui-popup-*custom properties — theme the popup once and every field component’s panel follows. The panel fades in and out, the way Menu already animated:@starting-stylesupplies the state to animate from and a discretedisplaytransition keeps it laid out through the fade, tuned with--cui-popup-transition-duration/-timing. Its anchored placement is unchanged; a fullscreen mode for mobile is designed at this primitive and lands separately. -
Validation and sizing ride the standard machinery.
.is-invalid/.is-validon the group behave exactly as on a.form-control, and the state also propagates from the control inside it. The per-family!importantoverrides that forced the invalid border in v5 are gone. -
Markers are hidden with
list-style-type: "", notlist-style: none. Safari drops thelistrole — and VoiceOver stops announcing the item count — when a list is styled withlist-style: none. The empty string hides the markers with the same visual result while keeping the list exposed to assistive technologies. Swept through nav, navbar, header, sidebar-nav, dropdown, breadcrumb and thelist-unstyledmixin; your ownlist-style: noneoverrides still win, they just reintroduce the problem. -
Transitions are emitted inside
prefers-reduced-motion: no-preference. Thetransition()mixin used to emit a transition and then switch it off again underprefers-reduced-motion: reduce, so every animated rule shipped an override block. It now emits the transition inside ano-preferencequery instead, which drops those blocks. The trade-off: a browser that does not support the media feature gets no transition at all. Rules that forcetransition: nonestay outside the query — they disable motion on purpose, so they have to reach every reader. -
The sizing utilities spell the property out:
.mw-100→.max-w-100,.mh-100→.max-h-100. The abbreviations were the only ones on that axis: the floor classes are.min-vw-100,.min-vh-100,.min-dvw-100, and the new.min-w-*/.min-h-*join them, so.mwsitting next to.min-wread as two conventions for one property. Both ends are now written the same way. -
.link-offset-*is.underline-offset-*. Same three steps, same-hovervariants —.link-offset-2-hoverbecomes.underline-offset-2-hover. The class setstext-underline-offseton any element, not just a link, and.underline-thickness-*next to it already carried the property-based name. -
New utilities:
.border-x/.border-y(and their-0forms) set theborder-inline/border-blockshorthands, so a facing pair of borders takes one class instead of two;.min-w-{0|100}and.min-h-{0|100}set a floor —.min-w-0is what releases themin-width: autoon a flex child so a.text-truncatesibling can actually truncate;.animation-{shake|pop}ship their keyframes with them, prefixed so they cannot be redefined by a bareshakeorpopof your own, and stand down underprefers-reduced-motion: reduce;.color-scheme-{light|dark}sets CSScolor-scheme, which is what the browser reads to paint form controls and scrollbars — it changes no colours of its own. -
.link-underline-*is.underline-*..link-underline-dangerbecomes.underline-danger, and the opacity steps become.underline-{n}(.link-underline-opacity-50-hover→.underline-50-hover). Like.underline-offset-*, the class sets atext-decoration-*property on any element rather than only on a link, and it now sits beside.underline-thickness-*, which already carried that name. -
Breaking The colour utilities take upstream’s names, and opacity is a step of the same family. Text colour is
.fg-{color}(.fg-emphasis-{color},.fg-contrast-{color},.fg-reset, plus the.fg-body/.fg-1….fg-4ladder that already carried the name); the light surfaces are.bg-subtle-{color}and.bg-muted-{color}, the light border.border-subtle-{color}. Opacity is no longer a separate-opacity-family with its own token:.fg-{n},.bg-{n},.border-{n},.link-{n}(.link-{n}-hover) and.underline-{n}(.underline-{n}-hover) run from10to100in steps of ten and mix the colour the companion class wrote into--cui-fg/--cui-bg/--cui-border/--cui-link-color/--cui-underlinewithtransparentthroughcolor-mix()..lh-mdis the body line height (.lh-smand.lh-lgread the type scale’s stops) and.shadow-opacity-*counts the same ten steps.Every v5 spelling still ships as a deprecated alias and renders the same, so existing markup is unaffected until v7:
.text-{color},.text-{color}-emphasis,.text-body,.text-black,.text-white,.text-reset,.text-body-secondary,.text-body-tertiary,.bg-{color}-subtle,.bg-{color}-muted,.border-{color}-subtle,.text-opacity-*,.bg-opacity-*,.border-opacity-*,.link-opacity-*(-hover),.underline-opacity-*(-hover),.lh-base,.shadow-opacity-25and-75. The deprecated opacity steps keep their old values (25,75); the new scale rounds them to30and70.What does change: the
--cui-text,--cui-text-opacity,--cui-bg-opacity,--cui-border-opacity,--cui-underline-opacityand--cui-link-opacitytokens are gone (--cui-textis--cui-fg), so an inlinestyle="--cui-bg-opacity: .5"no longer fades a.bg-*— use.bg-50, or set--cui-bgto a translucent colour. Reboot’sareadsvar(--cui-theme-fg, var(--cui-link-color))directly instead of mixing the link colour through--cui-link-opacityon every link. -
Breaking
.rounded-*counts the stops of the radius scale. The numbers are the keys of$radii(--cui-radius-0…-9) instead of the five v5 sizes, so every numbered class changes value and four new ones appear:class v5 v6 closest v6 class for the old value .rounded.375rem.5rem(--cui-radius-5).rounded-4.rounded-1.25rem.125rem.rounded-3.rounded-2.375rem.1875rem.rounded-4.rounded-3.5rem.25rem.rounded-5.rounded-41rem.375rem.rounded-9.rounded-52rem.5remnone ( .rounded-9is1rem).rounded-6….rounded-9— .625rem,.75rem,.875rem,1rem— The same numbering applies to
.rounded-size-*and the side classes (.rounded-top-*,.rounded-end-*,.rounded-bottom-*,.rounded-start-*);.rounded-0,.rounded-circleand.rounded-pillare unchanged. Each class writesvar(--cui-radius-{n})rather than the stop’s value, so a stop overridden on:rootre-rounds the utilities along with the components. The side classes set logical corners (border-start-start-radius, …), so.rounded-topflips with the writing direction like.rounded-startalready did. -
Buttons and controls carry a line height per size.
:rootemits--cui-btn-input-xs-line-height(1.125),-sm-line-height(--cui-line-height-sm) and-lg-line-height(--cui-line-height-md), the size classes hand them to--cui-btn-line-height/--cui-control-line-height, and each size’smin-heightis computed from its own line height.smandlgkeep the body value; the new.btn-xsis the only tighter size.--cui-border-width-keyline(.5px) is a root token now and.border-keylinereads it. -
New
.rounded-size-*. It writes the radius into--cui-rounded-sizewithout settingborder-radiusitself, and the unprefixed side classes (.rounded-top,.rounded-end,.rounded-bottom,.rounded-start) read that token, so one size class drives whichever corners you round. With no size in scope they fall back to--cui-radius-5. A numbered.rounded-{n}writes the token as well, so.rounded-3 .rounded-topkeeps the top on size 3; a side class with its own number (.rounded-top-3) sets that side directly. -
The derived colour utilities compose with their opacity classes.
.bg-*-subtle,.bg-*-muted,.border-*-subtleand.text-*-emphasiseach wrote the property directly, so.bg-opacity-50on a.bg-primary-subtledid nothing while the same class on.bg-primaryworked. They now declare--cui-bg/--cui-border/--cui-textand resolve through the samecolor-mix()as the base families. Nothing changes at full opacity.