# 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/dom` replaces it.**
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  Every positioned component — tooltips, popovers, dropdowns, autocomplete,
  multi-select, and the pickers — now positions with
  [Floating UI](https://floating-ui.com). `@popperjs/core` is no longer a peer
  dependency and the bundle builds (`coreui.bundle.*`) no longer include it.
  What to change:
  - npm: replace `@popperjs/core` with `@floating-ui/dom` in your dependencies.
  - CDN (non-bundle builds): replace the `popper.min.js` script tag with the
    two Floating UI files, in this order: `floating-ui.core.umd.min.js`, then
    `floating-ui.dom.umd.min.js`.
  - The `popperConfig` option (tooltip, popover, dropdown) is now
    `floatingConfig` and receives a Floating UI
    [computePosition](https://floating-ui.com/docs/computePosition) config —
    Popper's `modifiers` array does not translate; use Floating UI middleware.
  - The tip and menu carry `data-coreui-placement` instead of Popper's
    `data-popper-placement`; CSS or JS keying on the old attribute needs the
    new name.
  - Positioning is now asynchronous (`computePosition` is promise-based).
    Code that read the tip's coordinates synchronously right after `show()`
    should wait for the `shown` event.

### Distributed files

- **The `*.rtl.css` builds are gone.**
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  One stylesheet now serves both directions, so `dist/css` no longer carries
  `coreui.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&nbsp;kB
  stylesheet.

  **What to change:** drop the `.rtl` from your stylesheet link and keep
  `dir="rtl"` on `<html>`. `$enable-rtl: false` still produces an LTR-only
  build and `$enable-ltr: false` an RTL-only one, so nothing that was possible
  before stops being possible.

- **The Bootstrap theme and the Bootstrap-compatible JavaScript are gone.**
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  `dist/css/themes/bootstrap/bootstrap.css`, `dist/js/bootstrap.js`,
  `bootstrap.bundle.js` and `bootstrap.esm.js` no longer ship, with their
  minified twins, source maps and SRI hashes, and neither does
  `scss/themes/bootstrap/bootstrap.scss`. The two halves no longer agreed in
  v6: the JavaScript wrote `data-bs-*` attributes and `--bs-*` properties
  where the stylesheet still read `data-coreui-*` and `--cui-*` ones.

  **What to change:** load `coreui.min.css` and `coreui.bundle.min.js` (or
  `coreui.min.js`), write `data-coreui-*` attributes, listen for
  `*.coreui.*` events, reach the plugins through `coreui` instead of
  `bootstrap`, and set `--cui-*` properties instead of `--bs-*`.

- **`dist/css` gained `base.css` and a stylesheet per component.**
  `base.css` holds the `:root` tokens and Reboot; `css/components/` holds one
  file per component plus `manifest.json`, which records each file's load order
  and the components it needs alongside it. Nothing was taken away —
  `coreui.css` still 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](https://coreui.io/bootstrap/docs/getting-started/install/#per-component-css-files).

- **`dist/css` gained `coreui-legacy.css`, and `coreui.css` lost the v5
  spellings it carries.**
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  `coreui.css` is 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-check` on the input — moved to `coreui-legacy.css`, 3.4&nbsp;kB gzip.
  That takes 2.1&nbsp;kB gzip off `coreui.css` for 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, after `base.css` and the component files):

  ```html
  <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](https://coreui.io/bootstrap/docs/getting-started/install/#v5-compatibility-css).

- **`$enable-deprecated-classes` and `$enable-deprecated-form-check` are gone.**
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  A separate stylesheet answers the question both flags existed for, without a
  recompile: `false` is now "do not load `coreui-legacy.css`". Sass builds that
  set either flag fail with an unknown-variable error — delete the line.

### Browser support

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

**The 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&nbsp;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:

```ts
import { Modal, MultiSelect } from '@coreui/coreui-pro'

const modal = new Modal('#example', { backdrop: 'static', keyboard: false })
const version: string = Modal.VERSION
```

Each 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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](https://coreui.io/bootstrap/docs/guides/webpack/),
[Vite](https://coreui.io/bootstrap/docs/guides/vite/) and [Parcel](https://coreui.io/bootstrap/docs/guides/parcel/)
guides tell you to import:

```js
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:

```diff
- 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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:

```diff
- 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`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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:

```diff
- 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:

```diff
- <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:

```diff
- myCollapseEl.addEventListener('shown.coreui.collapse', () => {
-   // the area is expanded
- })
- collapse.show()
+ await collapse.show()
+ // the area is expanded
```

Collapse, 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)`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`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:

```diff
- 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
  `TypeError` the constructor throws, and the component is left as it was;
  Autocomplete and MultiSelect skipped that check on `update()`.
- **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()`
  and `setRange()` of Date Range Input, Date Range Picker and Calendar, and
  `setCalendarDate()` and `setSelectEndDate()` of Calendar change the value
  without `dateChange`, `timeChange`, `startDateChange`, `endDateChange`,
  `calendarDateChange`, `selectEndChange` or the native `input` and `change` of
  the value field;
  `errorChange` still 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`,
  `required` or `readonly` means disposing the instance and creating a new one.
  `update()` applied some of them, such as `format` on Date Input or `size` on
  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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`getSelected()` 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:

```js
chipSet.getSelected().join(',') // [object HTMLDivElement],[object HTMLDivElement]
transfer.getSelected()          // TypeError: Cannot read properties of undefined
```

Every getter now carries the noun it returns:

```diff
- 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`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`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`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::picker-icon),
`::picker(select)`, `showPicker()`):

```diff
- 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Four 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.

```diff
- 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Seven 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`:

```js
// 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`:

```diff
- 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](https://coreui.io/bootstrap/docs/accessibility/screen-readers/#announcing-messages).

### Components

#### Accordion on the native `<details>`

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The accordion renders on the native `<details>`/`<summary>` elements now.**
  `.accordion-item` goes on a `<details>` element and `.accordion-header` on its
  `<summary>`; the browser opens and closes the panel, so the component needs no
  initialization and works on markup added after load.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.accordion-button`, `.accordion-collapse` and `.collapsed` are 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-header` instead of `:not(.collapsed)`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Grouping moves from `data-coreui-parent` to the `name` attribute.** Give the
  `<details>` elements of one accordion the same `name` to keep a single item
  open at a time; omit it for an always-open accordion. The `id` plumbing
  (`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: -1px` on `.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.js` supplies
  the same animation in Firefox and Safari; that part of it goes away, without a
  breaking change, once they ship `calc-size()`. The component's events and
  methods do not depend on it and stay either way.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The accordion's events replace Collapse's.** `shown`/`hidden.coreui.accordion`
  fire on the `.accordion-item` whenever its open state changes, mirroring the
  native `toggle` event but bubbling, so they can be delegated. There is no
  `show`/`hide` counterpart: `<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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The plugin replaces Collapse's, and it is instantiated on the `.accordion`
  container, not on a panel.** Where v5 called
  `Collapse.getOrCreateInstance(panel).show()`, v6 calls
  `Accordion.getOrCreateInstance(accordion).show(item)`, with the item given as
  an element or as its index. `hide()` and `toggle()` take the same argument,
  and `showAll()` / `hideAll()` open and close the whole accordion — the latter
  pair also handles the `name` juggling the HTML Standard requires, which is why
  expanding an exclusive accordion is a plugin call rather than a loop over
  `open`. 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-duration` and
  `--cui-accordion-content-easing`, which drive the panel transition and are
  read back by `accordion.js` so 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-icon` selects the
  shape shown while the item is open, and a second element marked
  `.accordion-icon-active` does 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The open item is neutral, and `.theme-{color}` is what tints it.**
  `--cui-accordion-active-bg` and `--cui-accordion-active-color` were 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-2` and `--cui-fg-body` now, and the active tokens resolve
  through `--cui-theme-fg` / `-bg-subtle` / `-border` first — so the colour is a
  decision you make per accordion instead of one baked into the component. To
  keep the old look, put `.theme-primary` on the `.accordion`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-accordion-inner-border-radius` is gone; the header's corners follow
  `--cui-accordion-border-radius`.** The token was computed from the global
  `--cui-border-radius` when the stylesheet was built, so setting
  `--cui-accordion-border-radius` rounded 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.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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` / `-y` and `--cui-accordion-font-size` are 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 `.accordion` as `var(--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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The breadcrumb has no bottom margin.** `.breadcrumb` sat on
  `margin-bottom: 1rem` through `$breadcrumb-margin-bottom` and
  `--cui-breadcrumb-margin-bottom`; both are gone and the list ends where its
  content ends, like every other navigation component. Add `.mb-5` (or set
  `margin-bottom` on the `<nav>`) where the page relied on that gap.
- **Three breadcrumb tokens have real defaults.** `--cui-breadcrumb-bg`,
  `--cui-breadcrumb-border-radius` and `--cui-breadcrumb-font-size` were `null`
  in Sass, so the first two were emitted empty (`--cui-breadcrumb-bg: ;`) and
  the third was not emitted at all. They are `transparent`,
  `var(--cui-radius-5)` and `inherit` now. 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: 0` to square it off.
- **`--cui-breadcrumb-divider` is 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 `.breadcrumb` beats 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-flipped`
  defaults to the main divider instead of to a second copy of the Sass default,
  so setting only `--cui-breadcrumb-divider` no 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 for `role="menu"` there.
- **New: `.breadcrumb-divider`, a separator you can put in the markup.** The
  automatic `::before` divider 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-item` stops matching), so the two never
  double up, and the spacing is identical either way. Give the element
  `aria-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-bg` and `--cui-avatar-color` are 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 for `bg-{color}` plus a hand-picked `text-*`,
  which fails WCAG AA on five of the six examples they shipped — measured 1.85
  on `bg-warning text-white`, 2.94 on info, 3.50 on success, 3.69 on danger,
  and 3.12 on `bg-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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **One size token replaces the width/height pair.** `--cui-avatar-width` and
  `--cui-avatar-height` always 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` / `-height` become
  `--cui-avatar-status-size` the 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-size` still overrides it where a
  size needs its own.

  The Sass variables follow: `$avatar-width` / `$avatar-height` /
  `$avatar-font-size` / `$avatar-status-width` / `$avatar-status-height` become
  `$avatar-size` / `$avatar-status-size` plus `$avatar-font-size-ratio`, and
  `$avatar-sizes` keys drop to `size` and an optional `status-size`. The
  `$avatar-widths` map, deprecated since 5.1.0, is gone.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.avatar-img` fills the avatar and crops instead of stretching.** It was
  `height: auto`, which deformed every image that was not square; it is now
  `height: 100%` with `object-fit: cover`. Square images look the same.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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
  uses `transform`, so nothing else shifts. `$avatar-transition` changes from
  `margin .15s` to `transform .15s ease-in-out` accordingly.
- **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-xs` through `-xl` size every avatar in a stack at once.
- **The status indicator follows `.theme-{color}` and is never invisible.**
  `.avatar-status` reads 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **An alert without a theme class carries its own surface.** `--cui-alert-bg`
  falls back to `var(--cui-bg-1)` instead of `transparent`, so a plain
  `.alert` no longer takes the color of whatever it sits on. Set
  `--cui-alert-bg: transparent` on the alert to restore the old rendering.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The alert lays its children out in a row.** It is `display: flex` with
  `align-items: start` and `--cui-alert-gap` between children, so an icon next
  to a label needs no `d-flex` and 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`:

  ```html
  <div class="alert alert-success" role="alert">
    <div class="vstack">
      <h4 class="alert-heading">Great job!</h4>
      <p>…</p>
    </div>
  </div>
  ```

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A solid alert is `.alert-solid`, not a pile of utilities.** The docs used to
  say to drop the contextual class and reach for `bg-{color}` plus a `text-*`
  class picked by hand — and the examples hard-coded `text-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-solid` reads 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 show` fades in when it is added to the page.** The
  classes used to animate the alert in one direction only, because an element
  inserted with `.show` already on it has no earlier state to transition from;
  `@starting-style` supplies 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-close` that is a **direct child** of the alert still lands where it
  did, with or without `.alert-dismissible`, and `.alert-dismissible` itself 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-color` from the theme's border colour, falling
  back to `--cui-alert-border-color`, so a separator in a `.theme-success`
  alert 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-solid` paints its
  border with the surface colour, so there the separator takes the theme's
  border colour, or the text colour without one.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-alert-border` is gone.** The border is written in the rule from
  `--cui-alert-border-width` and the border colour, so that a `.theme-*` class
  can colour it. Set `--cui-alert-border-width` and `--cui-alert-border-color`
  instead, and `border-style` on the alert for a dashed or dotted line.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$alert-dismissible-padding-r` is 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-x` widened
  three sides of a dismissible alert and left the fourth on the default: at
  `2rem` the 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)` and
  `calc(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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`.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:

```diff
- 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-filter` is removed**, together with `$btn-close-filter` and
  `$btn-close-filter-dark`. Recolour the button with `--cui-btn-close-color`
  instead of inverting the image.
- **`$btn-close-color` defaults to `inherit`**, 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.btn-close-white` is 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-inverse` toast, a dark navbar's offcanvas, a `.bg-body.text-body`
  section under `data-coreui-theme="dark"`. Delete the class; the button
  renders identically without it. The rule additionally got the mirror case
  wrong: a `data-coreui-theme="light"` section inside a dark page inherited the
  forced white and drew a white icon on white.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-btn-close-width` and `-height` become `--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 still `1em` inside `.25em` of padding, so the pointer target stays
  24px at the default font size — the WCAG 2.2 minimum.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-btn-close-bg` is `--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-icon` carries the shape only. A custom image works
  the same way — give it any colour, the mask ignores it.

#### Badge

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Colour comes from `.theme-*`, and `.badge-*` now names the variant.** A bare
  `.badge` used 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-color` is `inherit` and `--cui-badge-bg` is
  `--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-subtle` and
  `.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 `.badge` with 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-danger` or add a text
  colour utility.

- **The badge lays itself out.** `display` is `inline-flex` with
  `--cui-badge-gap` between children and `--cui-badge-min-height` as 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-size` is
  `clamp(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 the `position-absolute top-0 start-100
  translate-middle` utility stack the docs used to recommend; that stack still
  works. The `.btn .badge` nudge (`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 .badge` with 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-outline`
  switches, `.badge-lg` joins `.badge-sm` on the size scale, and `a.badge` no
  longer inherits the underline from the reboot.

#### Carousel slides by scrolling

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-end` and `.pointer-event` no longer
  exist; react to the `slide`/`slid` events instead of styling them. The
  `.slide` class 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`ride` is `autoplay`, `wrap` is `ends`, and `touch` is gone.**
  `data-coreui-autoplay="true"` (boolean — the `"carousel"` spelling is gone)
  cycles from page load; `ends` takes `"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-gap` and `--cui-carousel-items-peek` put several
  slides in the viewport; `.carousel-center` snaps the active slide to the
  middle and `.carousel-auto` lets slides keep their own width. A button
  with `.carousel-control-play-pause` (holding `.carousel-control-pause-icon`
  and `.carousel-control-play-icon`) toggles autoplay and swaps its
  `aria-label` between `data-coreui-pause-label` and
  `data-coreui-play-label`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.carousel-dark`, the `carousel-dark()` mixin and `$carousel-dark-*` are
  gone.** The chrome colours are `light-dark()` pairs resolved on the
  carousel itself, so the standard `data-coreui-theme="dark"` attribute is
  the dark variant — and `data-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-duration` and
  `$carousel-transition` are gone; the fade reads
  `--cui-carousel-fade-duration` and `--cui-carousel-fade-timing`, and the
  slides stack on a grid instead of floating.

#### Autocomplete and Multi Select share one options menu

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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 classes
  `selected` and `indeterminate`. The former `autocomplete-dropdown`/
  `autocomplete-option(s)`/`autocomplete-optgroup*` and
  `form-multi-select-dropdown*`/`-options*`/`-option*`/`-optgroup*`/`-all*`/
  `form-multi-selected`/`form-multi-select-indeterminate` classes 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The options list is now a [List box](https://coreui.io/bootstrap/docs/components/list-box/) instance, so
  the `combobox-*` option classes are gone.** `.combobox-popup` stays 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-empty` |
  | `data-value` on an option | `data-coreui-value` |

  The 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-width` is left on `.combobox-popup`. Option
  padding follows the List box (`.5rem .75rem`, indicator in its own grid
  column) rather than the wider `1.25rem` the combobox surface used.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Multi Select's `selectAllStyle` and `optionsGroupsStyle` are removed.**
  A list has one marker, and `indicator` decides it for the options, the
  selectable group labels and the select all button alike. Replace
  `selectAllStyle: 'text'` / `optionsGroupsStyle: 'text'` with
  `indicator: 'none'` when you want the whole list without checkboxes.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Autocomplete keeps the focus in its input while the options list is
  open.** Options take no `tabindex` and are never focused; the input carries
  `aria-activedescendant`, and the arrows, <kbd>Enter</kbd> and
  <kbd>Space</kbd> are handled there, so <kbd>Home</kbd>, <kbd>End</kbd> and
  the space bar keep working as text keys.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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 when `search` is on, otherwise onto the option the list points
  at. Options take a roving `tabindex` and the arrows move the focus itself,
  so there is no `aria-activedescendant` to read any more.
  <kbd>Tab</kbd> cycles through the panel and the search box without leaving
  them, and <kbd>Escape</kbd> 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 <kbd>Tab</kbd>, and carries `aria-pressed`
  (`true`/`false`/`mixed`); selectable group labels are in the arrow order in
  its place.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Multi Select's `search` takes `true`, `false` or `'external'`, and `search: 'global'` is gone.**
  Typing reaches the search box from anywhere in the component by default,
  which is what `'global'` used to turn on, so `data-coreui-search="global"`
  becomes `data-coreui-search="true"`. Set `globalSearch: false` to 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 a `TypeError`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Multi Select's `value` wins over options marked `selected`.** It used to
  add to them, in the markup and in `options`, and later options applied the
  start value again; now a given `value` is 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, and `value: ''` reads
  like an empty `data-coreui-value`.
- **Multi Select takes `search: 'external'`.** The search box then filters
  nothing, `search.coreui.multi-select` carries the `query`, and
  `setOptions()` 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 a
  `loadingLabel` row below the options meanwhile, and they stay usable. `resetSelectionOnOptionsChange` empties the selection when the new
  options carry different values, for options that depend on another field.
- **Multi Select's `search.coreui.multi-select` follows 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 from `options` in the config,
  or one whose options `setOptions()` replaced, used to keep
  the generated options; an empty `<select>` given `options` is empty again
  after `dispose()`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Multi Select sends one `change.coreui.multi-select` per 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 native `change` works. 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, from `setValue()` or from a given
  `value` that options from `setOptions()` bring later, and a native reset of
  the form that changes the selection sends one, as Autocomplete, Chip Input
  and Combobox do.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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's
  `reset()`, Autocomplete's `clear()` and Range's `changed.coreui.range` used
  to fire even when the value stayed; they fire only when it changes now, and
  Range no longer fires while it starts or on the `change` after an `input`.
  OTP Input and Range Slider now fire on a native form reset that changes the
  value, and Date Input sends the native `input` and `change` of 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 later `setValue()` gave, and OTP Input's `reset()` 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 marked `selected`. A selected option that is disabled, also inside a
  disabled group, shows as a disabled chip and stays through the clear button,
  `deselectAll()` and <kbd>Backspace</kbd>; 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 its `value=""` 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Autocomplete's `search` takes only `'external'`, and global typing is
  `globalSearch`, on by default.** The array and the comma list are gone, so
  `search: ['external', 'global']` and `data-coreui-search="external, global"`
  become `search: 'external'`, and `search: 'global'` goes away: typing on the
  picker or clear button, or in the panel, reaches the field without it. Set
  `globalSearch: false` to keep a keystroke where the focus is. An array now
  fails the option type check, so `new 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 a
  `tabindex` the 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. With
  `search: 'external'` the panel opens for the options you set even when they
  do not contain the typed text.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Autocomplete's typeahead hint no longer reaches the server.** With
  `showHints` on, the hint input behind the field carried a `name` and, being
  readonly rather than disabled, was serialized with the form: the configured
  `name` when there was one, `<uid>-hint` when there was not. A form therefore
  submitted either the field twice with an empty string in front of the value,
  or an extra `…-hint` key nobody asked for. The hint now carries no `name`, so
  `new FormData(form).getAll(name)` returns one entry and `form.elements[name]`
  is an input rather than a `RadioNodeList`. Code written around the old shape,
  such as reading `getAll(name)[1]`, needs the plain value.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Multi Select's search field no longer reaches the server.** The input that
  filters the options carried a `name` and 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 no `name` now, the `search-…` key is gone from the payload,
  and the input sets `autocomplete="off"` so the browser stops offering to fill
  it.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **An unnamed Multi Select stays out of the form.** A `<select>` the page had
  not named was given `name="<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 the `id` and the
  `name` the component added, where it used to leave both behind.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **An unnamed Autocomplete stays out of the form too.** Its input took a
  generated `name` when the option was not set, so a form carried a key nobody
  chose. It does not now. The generated `id` is unaffected.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Validation state comes from `validationState`.** `invalid` and `valid` are
  deprecated aliases of `validationState: 'invalid'` and `'valid'`, and
  `validationState` also takes a state from `$form-validation-states`. A given
  `'invalid'`, from the options or from an `.is-invalid` class 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 own `checkValidity()`, now shows on the frame, and the
  autocomplete is the only one that writes `aria-invalid` and the message link on
  its input: the form plugin and the stepper leave the input to it. See
  [Validation](https://coreui.io/bootstrap/docs/forms/autocomplete/#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](https://coreui.io/bootstrap/docs/components/menu/).
- **New component: [Context Menu](https://coreui.io/bootstrap/docs/components/context-menu/)** (PRO) — a
  right-click, long-press or <kbd>Shift</kbd>+<kbd>F10</kbd> inside an element
  with `data-coreui-toggle="context-menu"` opens a `.menu` at 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.dropdown` events keep working unchanged — the plugin shares
  Menu's engine underneath. Where their options overlap, Dropdown also
  understands Menu's additions (for example `container`). **Deprecation
  notice: Dropdown may be removed in v7** — prefer Menu for new work.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The responsive alignment classes `.dropdown-menu{-sm|-md|-lg|-xl|-2xl}{-start|-end}`
  moved to `coreui-legacy.css`, and `.dropup`/`.dropend`/`.dropstart` no 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 with `data-coreui-display="static"` and
  inside a navbar, because the plugin writes the resolved placement onto the
  menu as `data-coreui-placement` and the stylesheet positions the static menu
  from that attribute rather than from the wrapper class. `dropdown.css`
  loses every `@media`; the direction classes keep the caret and still decide
  the placement when no option is given. `.dropdown-menu-end` and
  `.dropdown-menu-start` stay.

  ```diff
  - <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](https://coreui.io/bootstrap/docs/components/list-box/)** (PRO) — a keyboard-driven
  list of values on the ARIA listbox pattern, with `none`/`single`/`multiple`
  selection, sections, descriptions, links, an empty state and an
  `activeDescendant` mode that leaves the focus in your own input.

#### Combobox (new)

- **New component: [Combobox](https://coreui.io/bootstrap/docs/forms/combobox/)** — a `.form-control` toggle next
  to a popup [List box](https://coreui.io/bootstrap/docs/components/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](https://coreui.io/bootstrap/docs/components/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](https://coreui.io/bootstrap/docs/forms/validation/#usage)** (PRO) — validates a form with
  `data-coreui-validate` and `novalidate` on submit: stops an invalid submit,
  marks the invalid controls with `.is-invalid` and `aria-invalid`, links their
  messages with `aria-describedby` and moves focus to the first. It replaces
  the starter script of the validation page, see
  [Validation](#validation-data-coreui-validate-replaces-was-validated).

#### Modal and Offcanvas on the native `<dialog>`

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Both components render on the native `<dialog>` element now**, opened with
  `showModal()`/`show()` and closed with `close()`. The browser provides the
  top layer (no `z-index` management), the `::backdrop`, focus trapping,
  background inertness and the dialog ARIA semantics — the markup drops
  `role`, `aria-modal`, `aria-hidden` and `tabindex="-1"`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The modal wrapper layers are gone.** `.modal > .modal-dialog >
  .modal-content` flattens to a single `<dialog class="modal">` holding
  `.modal-header`/`.modal-body`/`.modal-footer` directly. Size
  (`.modal-sm/-lg/-xl`) and fullscreen (`.modal-fullscreen*`) modifiers sit on
  the dialog itself. `.modal-dialog-scrollable` is renamed to
  `.modal-scrollable`; `.modal-dialog-centered` is 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 inert `display: contents` shell 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-content` are the one thing that cannot be preserved.
  **This transitional layer will be removed in v7.**
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.modal-backdrop` and `.offcanvas-backdrop` are gone** — the backdrop is
  the native `::backdrop` pseudo-element. Style it through the new
  `--cui-modal-backdrop-bg`/`--cui-modal-backdrop-opacity` and
  `--cui-offcanvas-backdrop-*` tokens. `$zindex-modal-backdrop` and
  `$zindex-offcanvas-backdrop` are removed.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Animation opt-in is inverted.** Modals animate by default; `.fade` is gone
  and `.modal-instant` / `.offcanvas-instant` disable animation instead. The
  auto-upgrade maps a missing `.fade` to `.modal-instant`. The offcanvas
  `.showing` state class is replaced by the native `[open]` attribute
  (`.hiding` remains for the exit transition).
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The Modal `focus` option 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 is
  `overflow: hidden` + `scrollbar-gutter: stable` on the root element, with
  the `dialog-open` class).
- **New capabilities:** a `modal: false` option opens the dialog non-modally
  (page stays interactive); a `cancel.coreui.modal` event 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-transition` and `$modal-show-transform` are 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.** `:root` emits `--cui-backdrop-color`,
  `--cui-backdrop-opacity` and `--cui-backdrop-blur`; the dialog's and the
  drawer's `::backdrop` mix 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-translucent` and
  `.backdrop-none` swap them from the markup. `.dialog-no-backdrop` and
  `.drawer-no-backdrop` keep working. The sidebar's backdrop tokens are
  `--cui-sidebar-backdrop-zindex/-bg/-opacity` now (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 and `dialog-*` / `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](https://coreui.io/bootstrap/docs/components/dialog/) and [Drawer docs](https://coreui.io/bootstrap/docs/components/drawer/).

#### 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-bg` is `--cui-primary-bg-subtle` (the
  theme's `bg-subtle` under 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's `bg-muted`), one step stronger than the
  band.
- **Changed.** **`minDate` and `maxDate` are compared by the day.** v5 compared
  a day cell at midnight with the instant, so a limit that carried a time of
  day, such as `minDate: 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 `firstDayOfWeek` other than `1` a 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 picked
  `2026W35`, and that value given back as `startDate` did not select the
  same row. The number, the picked value, the weeks of `startDate`, `endDate`,
  `minDate` and `maxDate`, and the range and its preview now all follow the
  row's majority week, so that row is `2026W36` and the picked date is its
  Monday, August 31, the date `2026W36` stands 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 `disabledDates` disables 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 first
  `disabledDates: [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 `minDate` and `maxDate` count 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, a
  `minDate` later than the first day of its row disabled that row, and with
  Tuesday to Thursday first a `maxDate` of `2026W36` disabled the row of week
  36.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`cellHover` is 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 for `mouseover`
  or `focusin` on its element: each day, month, quarter and year cell the
  calendar shows carries its date in `data-coreui-date`, in the
  `toDateString()` 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
  only `mouseout` or `focusout` tells that the pointer or the focus left.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`selectAdjacementDays` and `showAdjacementDays` are spelled `selectAdjacentDays`
  and `showAdjacentDays`** — the option keys, the `data-coreui-select-adjacent-days`
  / `data-coreui-show-adjacent-days` attributes and the picker passthrough alike. The
  typo is fixed in the React and Vue libraries in the same release.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`update(config)` is gone; the dates change through `setStartDate()`,
  `setEndDate()` and `setCalendarDate()`**, and the other options are fixed once
  the calendar starts. A call like `update({ startDate })` used to reset every
  other option (`range`, `calendars`, `locale`, …). See
  [Live changes go through setters, not `update(config)`](#live-changes-go-through-setters-not-updateconfig).
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Each grid is one tab stop, and `tabindex` no longer marks a selectable cell.**
  The arrows, `Home`, `End` and the page keys move within a calendar and Tab
  leaves it, which is what `role="grid"` promises. Every cell inside `minDate` /
  `maxDate` can take the focus, also one that cannot be picked: the keys stop
  on a date in `disabledDates` instead 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 carries `data-coreui-selectable`, and
  `tabindex="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]`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Day cells write `data-coreui-date` in the `toDateString()` form**
  (`"Thu Sep 17 2026"`), like the month, quarter and year cells, instead of the
  full `Date#toString()` value. A script that selected a day cell with
  `[data-coreui-date="${date}"]` needs `[data-coreui-date="${date.toDateString()}"]`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Picking a date no longer fires `calendarDateChange` and 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 to `startDateChange` / `endDateChange` for 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 Up` turn 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 `weekNumbersLabel` the 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 new `ariaWeekNumberLabel` option takes a function of
  the week number to name it yourself.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The years view names its double arrows with new `ariaNavNextYearsLabel` /
  `ariaNavPrevYearsLabel` options** (`'Next 12 years'` / `'Previous 12 years'`)
  instead of the year labels, which stay on the days, months and quarters
  views. A page that translates `ariaNavNextYearLabel` / `ariaNavPrevYearLabel`
  needs to translate these two as well.
- **A grid in which nothing can be picked is described by the new
  `ariaNothingToPickLabel` option** (`'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 `ariaSelectedDateAnnouncement` and
  `ariaSelectedRangeAnnouncement` options** (`'Selected date: {date}'`,
  `'Selected range: {start} to {end}'`); before, picking a date with
  <kbd>Enter</kbd> 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 as
  `September 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, so `fa-IR` used 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A cell drawn by `renderDayCell` no longer gets an `aria-label`**, so its
  content is what a screen reader reads. Every render function receives the
  cell's full name as `meta.label` (`Wednesday, August 12, 2026`,
  `August 2026`, `Q3 2026`, `2026`); put it in the cell, for example in a
  `visually-hidden` span next to a short visible label marked
  `aria-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
  `today` class, `aria-current="date"` or `meta.isToday`, so a two-month
  calendar marks one current date and a date picker opens on today only when
  its own month is shown. `renderDayCell` receives the full `meta` in week
  selection too, not only `label`.
- A cell that cannot be picked carries `aria-disabled="true"`: a day outside
  `minDate` / `maxDate` or in `disabledDates`, a week row, month, quarter or
  year outside the range, and in day selection a day among an adjacent
  month's days unless `selectAdjacentDays` is on. Such an adjacent day is no
  longer `aria-selected` either, and `renderDayCell` gets `isDisabled: true`
  and `isSelected: false` for 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, and `renderDayCell` gets `isInRange: true`
  for 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 is
  `aria-multiselectable`, and every day, week row, month, quarter or year of
  the selected range that can be picked carries `aria-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, and `meta.isSelected` still marks only the ends.
- In week selection, `renderDayCell` gets the `isDisabled`, `isInRange` and
  `isSelected` of the day's week row, the way the calendar draws it: every day
  of a selected week is `isSelected`, every day of a week in the range is
  `isInRange`, and a day is `isDisabled` only 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 `minDate` cannot: 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`, `0026` and `12/31/0099` given 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 was `26-05`), so it reads back as the same year.
- **Additive:** `startDateChange` and `endDateChange` now carry a `dateObject`
  property with the underlying `Date`, alongside the existing `date` property.
  `date` remains formatted per `selectionType` — a `Date` for day selection, but
  a string for week (`"2026W27"`), month (`"2026-01"`), quarter (`"2026Q1"`) and
  year (`"2026"`). Existing listeners are unaffected.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`startDateChange` and `endDateChange` no 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 fire `endDateChange` with `null` although no end had
  been picked, and clearing a range across a disabled day before a start was
  picked fired `startDateChange` with `null`. A day picked again is still
  reported.
- `.months` and `.years` are gone from the stylesheet. Their one rule hid a
  `.time-picker` that followed a `.calendar` panel 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`,
  `.quarter` and `.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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

**The pickers are rewritten.** `DatePicker`, `DateRangePicker` and `TimePicker`
are now a composition of the section-based input fields
([date input](https://coreui.io/bootstrap/docs/forms/date-input/)), the [calendar](https://coreui.io/bootstrap/docs/components/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 with `data-coreui-picker-action="today|clear|reset|close"`;
  actions with arguments (range presets) go through `picker.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`, `yearPlaceholder` and the rest.
- **Removed — `previewDateOnHover` and `separator` on 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, and `separatorIcon` /
  `separatorIconRtl` replace its icon.
- **Kept.** `timepicker: true` still 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. `seconds` narrows what the time half offers and
  `selectionOptions` is its escape hatch.
- **Changed.** With `timepicker`, a `minDate` or `maxDate` that carries a time
  of day bounds the time on its day: the time selects disable the hours,
  minutes and seconds before `minDate` or after `maxDate` there, 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-number` and `data-coreui-format` work on the picker
  element. `calendarOptions` / `inputOptions` remain as the programmatic escape
  hatch.
- **Every selection type has a matching mask.** Month and year selection narrow
  the mask to `MM/yyyy` and `yyyy`. 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 to `QQQ yyyy`
  (`Q4 2026`). The v1 text input rendered `2026W27`/`2026Q1`; the submitted
  value now follows the mask (set `format="yyyy-Www"` for an ISO-style
  field), and the event carries the v1-formatted string in
  `event.formattedDate` next to the `Date` in `event.date` (see "Picker events
  can be cancelled" below). The format parser gained `ww`
  (ISO week) and `q`/`QQQ` (quarter) tokens, plus quoted literals
  (`'Week' ww` — fixed text that may contain token letters, `''` escapes a
  quote).
- **Format comes from the mask.** `inputDateFormat` is replaced by `format`
  (forwarded to the field); `inputDateParse` still applies to pasted text.
- **Footer buttons are projected, `today`/`now` actions keep the v1 gating.**
  A projected button opting into the `today` action (`now` on the time picker)
  is disabled at initialization when its target falls outside
  `minDate`/`maxDate`/`disabledDates` — matching the v1 built-in button. This
  is one-way: the picker only ever sets `disabled`, never re-enables. The
  state of any other projected button is yours — the slot context exposes
  `isDateSelectable(date)` (`isTimeSelectable` on the time picker) for
  computing it. Programmatic dates (`setDate`, actions, the initial `date`
  config) run through the same min/max validation as typed ones: out of
  range, the field turns `is-invalid`, `errorChange` fires, and `getDate()`
  reports `null`. `dateChange` on the shells always carries the validated
  value, and `setDate()` 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
  private `picker._calendar` before the first open now finds `null`.
- **`setRange()` and `reset()` 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()` and `reset()` on the Date Picker already showed their date.

#### The pickers take their validation state from `validationState`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`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](https://coreui.io/bootstrap/docs/forms/date-picker/#validation),
[date range picker](https://coreui.io/bootstrap/docs/forms/date-range-picker/#validation) and
[time picker](https://coreui.io/bootstrap/docs/forms/time-picker/#validation).

#### 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>`.

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
`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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

The range picker no longer builds its two fields itself: it composes the new
[date range input](https://coreui.io/bootstrap/docs/forms/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-group` on the picker's own
  element now find it one level down; `.show`, `.date-range-picker` and the
  `data-coreui-toggle` attribute stay where they were. `aria-expanded` moved
  from the element to the toggle button, which also carries `aria-controls`
  and `aria-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']` becomes
  `ariaStartLabel` / `ariaEndLabel`, and `floatingLabels: ['Check-in',
  'Check-out']` becomes `startFloatingLabel` / `endFloatingLabel`.

  ```diff
  - 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`dateChange` / `startDateChange` / `endDateChange` on the Date Picker and
  Date Range Picker carry the same payload whichever way the date arrived.**
  `event.date` is the validated `Date` (or `null`) the field holds, and
  `event.formattedDate` is that value in the shape of `selectionType`. A
  selection from the calendar used to put the formatted value in `event.date`
  and the `Date` in `event.dateObject`, while a date typed into the field put
  the `Date` in `event.date` — the same listener saw two shapes.

  ```diff
  - const date = event.dateObject ?? event.date
  + const date = event.date
  ```

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The pickers emit their change events only when the value changes.** A
  pick of the value already selected, and `clear()` or `reset()` when the
  picker already holds that value, no longer fire `dateChange`,
  `startDateChange`, `endDateChange` or `timeChange`; v5 fired them on every
  pick and on every `clear()` and `reset()`. The new `setDate()`,
  `setTime()`, `setStartDate()`, `setEndDate()` and `setRange()` fire no change
  event at all.

- **`shown` and `hidden` on the pickers wait for the popup's transition**, like
  the modal, dialog, toast and collapse events: `shown` fires once the panel
  has finished opening (and not at all when it closes first), `hidden` once it
  has finished closing and left the page (and not at all when it reopens
  first). v5 fired both as soon as `show()` or `hide()` ran.

- **`show` and `hide` can 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 a `show`
  listener keeps the panel closed, in a `hide` listener keeps it open, and the
  matching `shown` / `hidden` does not fire. `hide` on a panel that is not
  shown no longer emits anything.

#### Date Input / Time Input

- **The `disabled` option is the only switch.** A `.disabled` class 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 to `update()`, and emits `dateChange` when 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 of `document.documentElement.dir`, so a `dir="rtl"` ancestor works
  and not only a document-level direction.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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, with `tabindex="-1"` and
  `aria-hidden="true"`; a textarea, unlike a text input, leaves Enter
  submitting a form that has no submit button. A `required` field 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 fires `input` and `change` when the
  date changes. This applies to the date and time pickers and to both ends of a
  date range as well. Code that selects `input[type="hidden"]` inside the field
  needs `.form-date-time > textarea`.

#### Time Picker

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The `roll` infix 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-col` and `.time-picker-roll-cell` are `.time-picker-col`
  and `.time-picker-cell`, with `--cui-time-picker-col-*` and
  `--cui-time-picker-cell-*` in place of their `-roll-` spellings.
  `.time-picker-roll` itself was always present on `.time-picker-body` and its
  declarations moved there, so the modifier is gone rather than renamed;
  `--cui-time-picker-body-padding` went with it, since the modifier zeroed the
  padding it set.

- **`seconds` governs the field as well as the panel, and both are on by
  default, as they were in v5.** v5 formatted its value with seconds, so
  `10:30:00 AM` is 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.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`hours` is 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 as `en-US`) `hours: [1, 3, 5, 7]` meant
  1, 3, 5 and 7 in the morning and in the afternoon, `12` meant 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]`, with `0` and `12` for `12`. 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-width` is now the
  narrowest a cell gets, and `--cui-time-picker-cell-padding-x` keeps the name
  off the cell's edges.

- The time selection body is now a shared primitive used by both pickers, so the
  `hours`/`minutes`/`seconds` filters and the 12/24-hour locale split behave
  identically in each. See
  [time picker](https://coreui.io/bootstrap/docs/forms/time-picker/) and the date picker's own
  [time half](https://coreui.io/bootstrap/docs/forms/date-picker/#with-time).
- **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-body` is a grid with
  the v5 areas once more, so `.date-picker-timepickers` spans 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`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The date picker's time controls carry its own name.** `.time-picker-inline-icon`
  and `.time-picker-inline-select` are `.date-picker-time-icon` and
  `.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: <kbd>Tab</kbd> reaches it once,
  <kbd>Arrow Left</kbd> and <kbd>Arrow Right</kbd> move between the parts and
  stop at the ends, and <kbd>Arrow Up</kbd> / <kbd>Arrow Down</kbd> still change
  the focused value. The date picker's time selects answer the same keys.
- **Each picker has one time rendering, and the `variant` option 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;
  `selectionOptions` still 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`/`confirmButton` and
  their class options give way to a projected
  `<template data-coreui-template="footer">` with
  `data-coreui-picker-action="now|clear|reset|close"`; `inputReadOnly`,
  `inputOnChangeDelay` and `placeholder` are 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's `update()`, 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

The 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`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`-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-control` now**, like every other control. The two
  were already the same thing everywhere but the chevron: measured across every
  computed property, `.form-select` and `.form-control` on the same `<select>`
  differed in exactly ten — the eight `background-*` 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-control` sets `appearance: none`, which removes the native one, and
  nothing put ours back. That is fixed by the same change.
- **New: `.form-control-caret`** puts the chevron on an element that is not a
  `<select>` — a button that opens a listbox, for instance.
- **`.form-select`, `.form-select-sm` and `.form-select-lg` keep working** as
  aliases of the `.form-control` classes, not as a second implementation.
  Deprecated in v6, removed in v7.

  ```diff
  - <select class="form-select form-select-lg">
  + <select class="form-control form-control-lg">
  ```

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The `$form-select-*` Sass variables are gone**, along with
  `scss/forms/_form-select.scss`. Most were pass-throughs to `$input-*` /
  `--cui-btn-input-*` and their `.form-control` equivalents 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The chevron is translucent, and its color is no longer a Sass variable.**
  `$form-control-select-indicator-color` and its dark counterpart are gone; the
  SVG carries `#00000080` and `#ffffff80`, and the control declares both images
  as `--cui-control-select-bg-img` and `--cui-control-select-bg-img-dark`, dark
  mode switching which one the `background-image` reads. 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 from `rgb(44, 49, 60)` to `rgb(127, 127,
  127)` in light mode and from `rgb(223, 223, 223)` to `rgb(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 with
  `background-clip: padding-box`, the value `.form-control` has 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` / `-lg` now carry the
  `min-height` their `.form-control` counterparts 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.form-label` and `.col-form-label` render medium and full-contrast by
  default.** `$form-label-font-weight` is `var(--cui-font-weight-medium)`
  (was `null` — labels inherited the surrounding weight) and
  `$form-label-color` is `var(--cui-fg-body)` (was `null`), so a label inside
  a muted or tinted context keeps the page's text colour instead of fading
  with it. Set the knobs back to `null`, or `--cui-label-font-weight` /
  `--cui-label-color` to `inherit`, for the old behaviour.

#### Validation: `data-coreui-validate` replaces `.was-validated`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
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-validate` alone shows only
  invalid styling; set `data-coreui-validate="valid"` to also colour the
  valid fields.
- **Server-side validation is unchanged** — `.is-invalid` / `.is-valid` keep
  working globally, without the attribute.
- **Feedback of a check, a radio, a switch, a `.form-control` or `.form-select`
  in an input group, or a control placed directly in a component frame belongs
  to the [`.form-field`](https://coreui.io/bootstrap/docs/forms/field/) around it.** The field shows it while
  the control carries the state, wherever it sits in the field, for example in
  a `.form-field-content` next to the label, and a check, a radio or a switch
  colours the label. Feedback right after any other control still follows it.

```diff
- <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](https://coreui.io/bootstrap/docs/forms/validation/#usage): 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](https://coreui.io/bootstrap/docs/forms/field/) 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()`](https://coreui.io/bootstrap/docs/accessibility/screen-readers/#announcing-messages).
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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
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](https://coreui.io/bootstrap/docs/forms/validation/#form-components):

- **The state comes from the `validationState` option**, or from `.is-invalid`
  or `.is-valid` on the component's element, read once when the component
  starts. A class or an `aria-invalid` written on the element later is not
  read; set the state with `setValidationState()`.
- **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()` on
  `validate.coreui.form`, keep `event.isValid` rather than calling
  `checkValidity()`: 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`--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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
`.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`](https://coreui.io/bootstrap/docs/forms/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.

```diff
- <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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`.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:

```diff
- > .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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

The 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>`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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](https://coreui.io/bootstrap/docs/forms/chip-input/#validation).

#### Chip Input takes the frame class itself

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

```diff
- <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](https://coreui.io/bootstrap/docs/forms/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-group`](https://coreui.io/bootstrap/docs/forms/form-control-group/) frame. Nothing to migrate —
  it is listed here because the `number-input` name, 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 `::before` pseudo-elements with
  their own variable set; they are decorative `.form-check-input` spans now
  (`aria-hidden`, since `aria-selected` carries 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-width` survives, and it follows the
  checkbox: `--cui-check-size` where the indicator can see it, and the shared
  `--cui-control-size` otherwise. Theme the option checkbox through `$check-*` /
  `--cui-check-*` instead.
- **Validation state comes from `validationState`, and the select is no longer
  watched.** `invalid` and `valid` 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 the user makes to the selection drops it. An `.is-invalid`
  or `.is-valid` class on the select is read once, when the component starts, and
  moves to the frame; a class or `aria-invalid` written on the select later is not
  read, so set the state with `setValidationState()`. A native form
  reset restores the selection the control started with. See
  [Validation](https://coreui.io/bootstrap/docs/forms/multi-select/#validation).
- **<kbd>Backspace</kbd> 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, <kbd>Backspace</kbd> or the cleaner is announced (`ariaRemovedAnnouncement`).
  A single select and the `counter` and `text` selections 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.
  `ariaTagDeleteLabel` is deprecated in favour of `ariaChipRemoveLabel`; the old
  option still names the remove button and takes precedence while it is set.

#### Password Input and Number Input build their own frame

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Both components supply their own buttons, so they supply the frame that lays
them out. The markup is a plain form control carrying the toggle:

```diff
- <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-toggle` moves from the button to the input**, and its value is
  the component's name: `password-input` (and `number-input` for the new
  [Number Input](https://coreui.io/bootstrap/docs/forms/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 on `dispose()`.
- **An input already inside a `.form-control-group` is left where you put it**,
  so a field can share a frame with other adornments, and only a frame the
  component created is removed on `dispose()`.
- The accessible name of the toggle is the `ariaToggleLabel` option now, since
  the button is no longer yours to label.

#### Password Input: the toggle icon comes from JavaScript

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

The 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:

```diff
  <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-icon` is gone, and `scss/forms/_form-password.scss`
  with 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-hide` and the matching
  `--cui-form-password-icon-*` properties are gone. Supply your own artwork
  through the new `showIcon` / `hideIcon` options instead, the way the other
  adornments already take theirs. Both are sanitized; `allowList`, `sanitize`
  and `sanitizeFn` control that.
- The plugin now initializes on `DOMContentLoaded` for markup carrying
  `data-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 CSS `mask` data-URIs: swap them with the
  `indicatorIcon` / `separatorIcon` options (sanitized with the SVG allowlist),
  recolor them with plain CSS `color`. The corresponding
  `$date-picker-*-icon`/`-icon-size` Sass 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 new `navPrevIcon` / `navNextIcon` /
  `navPrevDoubleIcon` / `navNextDoubleIcon` calendar options instead (inline SVG,
  sanitized). `$calendar-nav-icon-width/-height/-color/-hover-color` stay — the
  icon inherits `color`, so recoloring is plain CSS.
- **`Calendar`'s default `allowList` is now `SVGAllowlist`** (was
  `DefaultAllowlist`), 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
  between `separatorIcon` and `separatorIconRtl` when it renders, based on the
  element's computed direction — so a `dir="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 declares `dir` answers 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Date 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's
  `bg-muted` under a `.theme-*` class, instead of a 30% tint of the primary
  colour (`$form-range-thumb-active-bg`).
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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 an `undefined=` 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 `Range` plugin.**
  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-tooltips` adds a
  value tooltip on hover, drag and focus, or permanently with `"always"` (the
  same `tooltips`/`tooltipsFormat` options as the range slider), and a linked `<datalist>` renders tick
  marks laid out on a grid. Every such wrapper initializes itself. The input
  fires `changed.coreui.range` with the numeric `value`. 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-bg`
  and the `--cui-range-tick-*` family (labelled ticks use `-height`, unlabelled
  ones `-minor-height`).
- **The range track sits halfway between `--cui-bg-2` and `--cui-bg-3`**
  (`color-mix(in oklch, …)`, was `--cui-bg-3`), in both the native range and
  the range slider. `--cui-bg-3` and the `secondary` colour
  resolve to the same grey in light mode, so a `theme-secondary` thumb and
  fill vanished against the track.
- **Validation reaches the range.** `.is-invalid`/`.is-valid` on the input
  (either form) colour the thumb and its focus ring with the state, and a
  feedback element after the wrapper toggles through `:has()`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The range slider renders the markup of the range.** It builds
  `<div class="form-range">` inside its element, with one native
  `.form-range-input` per 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-tick` with the text in `.form-range-tick-label`, and the
  per-tick `.clickable` → `.form-range-ticks-clickable` on the container.
  `.range-slider-inputs-container` and `.range-slider-track` are gone (the
  first input draws the track), and so is the `.disabled` class on the element
  (the inputs are disabled); the stylesheet styles none of the
  `.range-slider-*` classes any more. The handles no longer carry `role="slider"`,
  `aria-valuemin`, `aria-valuemax` or `aria-valuenow`: the native inputs expose
  them. Options, events and methods keep their names, apart from `ariaLabels`
  (below); the `changed.coreui.range` event that an inner Range fired on each
  handle is gone, so listen to `input.coreui.range-slider` and
  `change.coreui.range-slider` on 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 { … }`).
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The range slider's `ariaLabels` option is `ariaLabel`.** 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-labels` becomes `data-coreui-aria-label`.
- **The range slider element is a group named after the field.** The
  component gives it `role="group"`, so an `aria-label` or `aria-labelledby` on
  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
  its `ariaLabel` when it has one. `aria-describedby` moves onto every handle.
  A `role` written on the element is replaced with `group`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The range slider takes a validation state.** The new `validationState`
  option, or `.is-invalid`/`.is-valid` on 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 with `setValidationState()`. See
  [Validation](https://coreui.io/bootstrap/docs/forms/range-slider/#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-slider` on release instead of on press; the events carry
  numbers; a form reset restores the configured values instead of the middle of
  the range; `setValue()` and `dispose()` touch only what the slider built, so
  content of the element stays.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-range` declares 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-color` and `-font-size`, the tooltips
  sit like the range's, a vertical slider's height is
  `--cui-range-vertical-height` and 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The range slider's `labels` option is `ticks`.** Every entry draws a tick
  mark, with or without text, so the option and everything around it carry the
  name the `Range` plugin 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 in `ticks` is a tick without
  text at that value. The new `list` option reads a `<datalist>` like the
  range does and merges its options with `ticks`. Custom CSS that
  positioned `.range-slider-tick` through `left`/`bottom` no longer applies.
- **Range slider tooltips follow the range's modes.** `tooltips: true` shows 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The value bubble is a tooltip.** The plugin renders
  `<div class="form-range-tooltip tooltip bs-tooltip-top">` (vertical:
  `bs-tooltip-start`, or `bs-tooltip-end` in a right-to-left page) with
  `.tooltip-arrow` and `.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-bottom` and `-margin-end`. The tooltip declares its own
  `--cui-tooltip-*` tokens, so set them on it: for the previous light bubble,
  give the slider `tooltipClass: 'light-tip'` and declare
  `--cui-tooltip-bg: var(--cui-bg-4)` and `--cui-tooltip-color: var(--cui-fg-body)`
  on `.light-tip`.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-bg` at
  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-tokens` and the variant maps), so the
  sidebar takes `@use ... with ()` overrides like every other component. The
  narrow variant's variables (`$sidebar-narrow-*`) live in
  `sidebar/_sidebar-narrow.scss`.
- **Hover and active links read the foreground ladder.** Their text was the
  pure black/white `--cui-emphasis-color` and their background the
  Sass-computed `--cui-tertiary-bg-translucent`; they are `--cui-fg-body` and
  `color-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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-tertiary-bg-translucent` is gone from `:root`.** The sidebar was
  its only reader. Set `--cui-sidebar-nav-link-hover-bg` /
  `-active-bg` on `.sidebar-nav` for a different tint.
- **Theme variants.** `.sidebar.theme-{color}` paints the surface, text,
  icons and the hover/active links from that colour's slots. `.sidebar-light`
  joins `.sidebar-dark` as the scheme switch.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.sidebar-body` and `.sidebar-form` are 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 is `hide()` from
  a resize handler, or the mobile mode the sidebar takes below `lg` on its own.
- **The mobile backdrop reads the shared backdrop tokens.** `.sidebar-backdrop`
  mixes `--cui-backdrop-color` at `--cui-backdrop-opacity` and blurs by
  `--cui-backdrop-blur`, like the dialog and the drawer; `--cui-sidebar-backdrop-bg`
  carries the mix and `--cui-sidebar-backdrop-blur` the blur, and
  `--cui-sidebar-backdrop-opacity` (`.5`, the old fade target) is gone. The
  `.backdrop-*` helpers work on the sidebar too.
- **`.sidebar-narrow` gets its bottom padding back.** It read
  `--cui-sidebar-toggler-height` from a token declared on the toggler, a
  descendant, so the padding silently fell to 0; the token is declared on
  `.sidebar` now.

#### ScrollSpy

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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 `topMargin` below
  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`** (default `12%`) 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`rootMargin` defaults to `null` and is now the raw override.** Set it and
  it is handed to the `IntersectionObserver` unchanged, taking precedence over
  `topMargin`; a v5 configuration such as `rootMargin: '0px 0px -25%'` keeps
  working as written. `threshold` defaults to `[0]` instead of
  `[0.1, 0.5, 1]` — with an activation line a section only needs to report
  whether it crosses it.
- **`smoothScroll` finishes the navigation.** Once the scroll settles, the
  URL hash is replaced with the link's target and focus moves to the section.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The deprecated `offset` option is removed** — it has been a thin alias
  since the IntersectionObserver rewrite. Use `topMargin` instead:
  `offset: 100` &rarr; `topMargin: '100px'`.

#### Tab

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Constructing a Tab outside a tab-panel parent now throws a `TypeError`**
  instead 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-transition` splits into
  `$nav-link-transition-property`/`-duration`/`-timing`**, emitted as
  `--cui-nav-link-transition-*`. The link's transition was compiled into its rule
  while `.tab-pane` in 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-duration` now takes effect; before it was ignored.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.nav-link` is 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-align` and
  `--cui-nav-link-justify`, so an icon and its label line up without a wrapper.
  It also takes `--cui-nav-link-hover-bg` and `--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 to `transparent` to keep the old look.
  Under a theme, hover reads `--cui-theme-fg` and 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-underline` and `.nav-underline-border` square 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.nav-tabs` draws its line with an inset `box-shadow`, not a border.** The
  links no longer pull themselves over the container's border with a negative
  `margin-bottom`; each link carries its own bottom border (the line colour at
  rest, the panel colour when active) and the container paints the line as
  `box-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-bottom` or the links' negative
  margin needs a second look; recolour the line through
  `--cui-nav-tabs-border-color`. `.card-header-tabs` compensates with a deeper
  negative margin instead of zeroing the border.
- **`--cui-nav-gap` is new**, declared on `.nav` and defaulting to `0`, so the
  base nav renders exactly as it did. It gives the token a home on the component
  whose name it carries: `.navbar-nav` already overrode it, and `.nav-underline`
  keeps its own `--cui-nav-underline-gap`.

#### Tooltip

- **`--cui-tooltip-opacity` now blends into the background colour instead of
  fading the whole element.** The old element-level `opacity: .9` also faded the
  label text, compositing it below the WCAG 1.4.3 contrast minimum (about
  3.5&#8758;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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$tooltip-margin` and `--cui-tooltip-margin` are removed.** The variable
  had been `null` and deprecated since v4; the token compiled to an empty
  value and the `margin` declaration reading it never took effect. Position
  a tooltip with the JS `offset` option instead.
- **Roomier tip, less see-through.** `--cui-tooltip-padding-y` is
  `calc(var(--cui-spacer) * .375)` (was `.25rem`),
  `--cui-tooltip-padding-x` is `calc(var(--cui-spacer) * .75)` (was `.5rem`) and
  `--cui-tooltip-opacity` is `.95` (was `.9`). Set the three tokens on
  `.tooltip` to keep the old proportions.
- **Tooltips take theme classes.** The surface and the text read the
  `--cui-theme-bg`/`--cui-theme-contrast` slots, with `--cui-tooltip-bg` and
  `--cui-tooltip-color` as the fallbacks, so a `.theme-{color}` class on the
  tip (via `customClass`) 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
  are `off`), 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 gained `enterkeyhint`,
  `autocorrect="off"` and `spellcheck="false"`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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 a `name` or an `id` written 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 own `value` attributes
  are submitted as well, which they were not before.
- **In a right-to-left field, `linear` now holds the forward move rather than
  the `ArrowRight` key.** Forward is `ArrowLeft` when the field reads
  right-to-left, so that is the key a linear field stops on an empty slot,
  and `ArrowRight` walks 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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, with `tabindex="-1"` and
  `aria-hidden="true"`, a `pattern` for the full length of the code and the
  `required` that the slots used to carry. A `required` code 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-invalid` and every slot carries `aria-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 selects `input[type="hidden"]` inside the field
  needs `.form-otp > input:not(.form-otp-control)`, a slot no longer gets
  `required`, and a `data-coreui-invalid-feedback` written on the slots goes
  on the `.form-otp` element.
- **The code takes a validation state.** The new `validationState` option, or
  an `.is-invalid`/`.is-valid` class on the `.form-otp` element 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 sends `input` and `change`
  from the input that holds the value.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The `readonly` option is removed, and every slot is editable.** A code the
  user cannot type has no use in a form, so `data-coreui-readonly`,
  `{ readonly: true }` and a `readonly` written on a slot no longer make it
  read-only; use `disabled` to show a code the user cannot change.

#### Rating

- **The read-only option is spelled `readonly`.** It was the one component in
  the library spelling it `readOnly`, which made its data attribute
  `data-coreui-read-only` while every other field component answers to
  `data-coreui-readonly`. Both spellings now agree with the HTML attribute:
  `data-coreui-read-only="true"` becomes `data-coreui-readonly="true"`, and
  `new Rating(element, { readOnly: true })` becomes `{ readonly: true }`. The
  `.readonly` class the component writes is unchanged, and the React and Vue
  props stay `readOnly` — 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 after `dispose()` used to keep
  working as a plain radio group.
- **Its options decide `.disabled`, `.readonly`, the size class, the
  `radiogroup` role and `aria-readonly`, `aria-required` and `aria-invalid`.**
  The same classes and attributes written in the markup are replaced at init,
  and a `rating-sm` or `rating-lg` without `size` is removed. `update({ disabled: false })` used to leave
  `.disabled` behind.
- **A read-only rating submits its value under the `name` you give it.**
  `readonly` used 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 carries `aria-readonly="true"`. A
  read-only rating without `name` still submits nothing.
- **A star with keyboard focus shows the focus ring.**
- **The setters keep the value the user picked and the stars.** `setValue()`,
  `disable()` and `enable()` change their part in place; any option change
  through `update()` used to render the stars again and reset the rating to its
  initial value.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The rating takes a validation state.** The new `validationState` option,
  or `.is-invalid`/`.is-valid` on the rating element read once when it starts,
  colours the focus ring, and a given `'invalid'` sets
  `aria-invalid` on 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 with `setValidationState()`. See
  [Validation](https://coreui.io/bootstrap/docs/forms/rating/#validation).
- **The rating can be required.** `required` makes every radio required and
  gives the group `aria-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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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]`, so `dispose()` lasted until the next key press.
  Now the buttons in the document at `DOMContentLoaded` are initialised, and a
  button added later, or one on a page whose script runs after
  `DOMContentLoaded`, responds to its shortcut once you call
  `coreui.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-keys` container written in the markup gets its own content
  back.

#### Alert

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Every alert fades out when dismissed, and `.fade` no longer decides that.**
  The alert carries its own transition and
  `--cui-alert-transition-property` / `-duration` / `-timing`; the JavaScript
  marks it with `.hiding` for the length of it, then removes the element. An
  alert is already on the page, so only the exit is animated — `.fade` and
  `.show` remain the way to fade one **in** when you insert it at runtime. Add
  `.alert-instant` to an alert that should go at once, which is what an alert
  without `.fade` used to do. Add `.transition-size` next to `.fade` and the exit takes the
  box with it, so the page rises into the alert's place instead of jumping.

- **`.alert-dismissible` is 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span> **A chip starts selected only
  from the `selected` option.** An `active` class in the markup no longer selects
  it; write `data-coreui-selected="true"` on the chip instead. The role,
  `tabindex` and `aria-*` a chip or a chip set writes come from its options too,
  so a role or `tabindex` in the markup is replaced rather than kept, and
  `dispose()` 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
  `-1` on the others and moves it with the focus, so <kbd>Tab</kbd> 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"` (with `aria-multiselectable` in multiple mode) whose chips
  are `role="option"` elements carrying `aria-selected`; a standalone
  selectable chip is a toggle button (`role="button"`) reflecting its state
  via **`aria-pressed` instead of `aria-selected`**; a non-selectable set is a
  `role="group"`. `aria-disabled` is only stamped on chips that carry a role —
  on a role-less chip the attribute was invalid and is now removed.
- **`clear()` and `removeSelected()` report the change once.** Chip set and Chip
  input fired `change` and `select` (`.coreui.chip-set`/`.coreui.chip-input`), and
  Chip input its native `input` and `change`, 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 cancelable `remove` still fires for every chip.
- **Adding and removing chips is announced.** Chip set and Chip input announce
  each change through `announce()`; the wording is configurable via
  `ariaAddedAnnouncement`/`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 is
  `Remove <text>` now, as Multi Select already did; Chip input names it after the
  chip's text instead of its value. `ariaRemoveLabel` (and Multi Select's
  `ariaChipRemoveLabel`) 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 as `Usuń` gains the text; a label that already spelled
  the text out, such as `data-coreui-aria-remove-label="Remove React"`, now repeats
  it, so drop it or use `{label}`. A `.chip-remove` button you write yourself keeps
  its `aria-label`, and gets one from `ariaRemoveLabel` when 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. <kbd>Backspace</kbd> /
  <kbd>Delete</kbd> remove it from the keyboard, the affordance from the pointer.
- **Chip input: `aria-disabled`/`aria-readonly` are no longer stamped on the
  container.** The container is a generic element, so those attributes were
  invalid on it (axe `aria-allowed-attr`); the native `disabled`/`readonly`
  states on the inner `<input>` carry the semantics.
- **The remove button gets its size back, and `.chip-sm`/`.chip-lg` resize
  everything they claim to.** `.chip-remove` sized itself from a token that was
  never declared, so both dimensions were invalid at computed-value time and fell
  to `auto` — the button measured 0x0 at every chip size. It reads
  `--cui-chip-remove-size` now (16px, 14px on `.chip-sm`, 20px on `.chip-lg`), so
  a chip with a remove button is wider than it was.
- **`.chip-dismiss` is `.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's `disabled` attribute
  was doing the work on its own.
- **`--cui-chip-dismiss-size` is 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-sm` and `.chip-lg` no longer swap the image and icon sizes.** They
  passed the two to `chip-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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The selected chip's text follows the palette.** `$chip-active-color` was
  white at 87% opacity, computed at build time — repaint the selected
  background and the text stayed near-white. It defaults to
  `var(--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-subtle` to `bg-muted` and the text
  colour stays put; only selection paints the solid theme colour. A chip
  without a theme class hovers exactly as before.

#### Collapse

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The `toggle` option is gone.** It made the constructor change the state the
  markup declares, which surprised people — so nearly every caller passed
  `toggle: false`. The constructor now always keeps the declared state; add
  `.show` to start open, and call `show()`, `hide()` or `toggle()` to change it.

  ```diff
  - 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-toggle` on a *trigger* is
  unrelated and unchanged.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Triggers no longer get the `.collapsed` class.** Their state was written
  twice, as the class and as `aria-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:

  ```diff
  - .accordion-button.collapsed::after { transform: rotate(-90deg); }
  + [data-coreui-toggle="collapse"][aria-expanded="false"]::after { transform: rotate(-90deg); }
  ```

- **New `hiddenUntilFound` option.** With `data-coreui-hidden-until-found="true"`
  a collapsed area is hidden with `hidden="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 `.show` and `aria-expanded` so 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 way `hidden` always did.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The transition is set through CSS variables, and the Sass variables split
  in two.** `$transition-collapse` and `$transition-collapse-width` were
  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-duration` and
  `$transition-collapse-timing`, emitted as `--cui-transition-collapse-property`
  / `-duration` / `-timing` on `.collapse` and `.collapsing`, so all three can
  also be changed per instance at runtime. `$transition-fade` splits the same
  way into `$transition-fade-duration` / `$transition-fade-timing`.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **CSS owns the collapse animation, and the class choreography follows.**
  Where `interpolate-size: allow-keywords` is supported the stylesheet
  interpolates the size to the `auto` of 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:

  - **`.collapse` never leaves the element.** It used to be swapped out for
    `.collapsing` while the transition ran; now `.collapsing` is added
    alongside it, so `.collapse.collapsing` matches during a run and a selector
    keying on the *absence* of `.collapse` no longer fires mid-animation.
  - **`.show` toggles at the start of both directions.** It used to land at
    the end of the opening transition. A rule styling `.collapse.show` applies
    for the whole expansion now, and its absence marks the whole closing run.
  - **An open `.collapse-horizontal` is `fit-content` wide, not as wide as its
    container** — the open size is the animation's target, and a target of
    `auto` would 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-pressed` now.** 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 on `DOMContentLoaded`, reading the `.active` class 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.btn` is 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-height`
  keeps a button that holds only an icon as tall as one that holds text. Custom
  CSS that positioned button content with `display`, `line-height` or margins
  needs a second look; the caret of a `.dropdown-toggle` now takes its distance
  from the gap instead of its own margin.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A button label no longer wraps.** `--cui-btn-white-space` is `nowrap`. In v5
  `$btn-white-space` was `null`, 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 set `nowrap`, 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-toolbar` spaces its groups on its own** with a `.5rem` `gap`
  (`--cui-spacer-3`). Drop the `me-*` 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:

  ```css
  .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 `.btn` itself, 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-wrap` grows
  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-ghost` take 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 — `.btn` is no longer required next to them:

  ```diff
  - <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-primary` is `.btn-solid.theme-primary`, `.btn-outline-primary` is
  `.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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A bare `.btn` is 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-shadow` and
  `--cui-btn-box-shadow` are removed; a `.btn` draws no `box-shadow` at 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`disabledOnLoading` no longer sets the `disabled` attribute.** 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 gets `aria-disabled="true"`
  and `.disabled`, keeps focus, and ignores clicks and form submission until it
  stops. CSS or scripts that read `:disabled` on 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 `ariaLoadingLabel` option, then its name
  again. While loading, the spinner is a `role="img"` named by
  `ariaLoadingLabel` instead of an empty `role="status"`.

#### Navbar

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.navbar-expand-*` reads the navbar's own width, not the viewport's.** Every
  `.navbar` is a [query container](https://coreui.io/bootstrap/docs/utilities/container-queries/) 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: nowrap` and `justify-content: flex-start` moved 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 inherit `flex-wrap`). A navbar written **without** an inner
  `.container`, `.container-fluid` or `.container-{breakpoint}` no longer gets
  them — wrap its content, the way every example in these docs does.
- **`--cui-navbar-text-padding-y` and `--cui-navbar-toggler-icon-size` are new.**
  The padding that lines `.navbar-text` up 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 `.navbar` now, 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 `.offcanvas`
  only, so a `.drawer` inside 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Three navbar defaults move.** `.navbar` takes
  `--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-brand` takes
  `--cui-navbar-brand-font-weight`, defaulting to medium rather than the body
  weight. `.navbar-nav` takes `--cui-nav-gap`, a `.25rem` gap 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-translucent` is new.** It tints and blurs the navbar the way
  `.toast-translucent` does 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-color` and `-active-color` carried 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`.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.

```diff
- <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`.

<span class="badge fg-emphasis-danger bg-subtle-danger">Removed</span>
**`.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`](https://coreui.io/bootstrap/docs/forms/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.

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
**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-top`
  used to be Sass arithmetic over `$form-check-input-width`, so changing the width
  token at runtime moved the box off the line. It is a `calc()` over the size
  token now.
- **Sized switches are token overrides.** `.form-switch-lg` / `-xl` set
  `--cui-switch-width` and `--cui-switch-height` instead 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.scss` and `_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](https://coreui.io/bootstrap/docs/forms/checkbox/), [Radio](https://coreui.io/bootstrap/docs/forms/radio/) and
  [Switch](https://coreui.io/bootstrap/docs/forms/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-switch` and `.form-check-inline` all work exactly as before;
  `.form-check-reverse` is removed.
- **New: [`.form-field`](https://coreui.io/bootstrap/docs/forms/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-check` wrapper) 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-inline`](https://coreui.io/bootstrap/docs/utilities/container-queries/) utility) — without one the
  direction never switches. Plain `.hstack` and `.vstack` are 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 a `var()` call, so nothing
  reported them and an override had to guess the name. `.img-thumbnail` and
  `.figure-caption` declare their full set on the element, padding, border width
  and caption font size included.
- **`--cui-figure-img-margin-bottom` is 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-img` declares it now, and it is the one figure
  measurement you can retune in the browser.

#### Figures

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.figure` is an inline flex column and spaces its parts with
  `--cui-figure-gap`.** `$figure-img-margin-bottom` and
  `--cui-figure-img-margin-bottom` are gone — the gap (knob: `$figure-gap`,
  same `.5rem`) replaces the image's bottom margin, and `.figure-img`
  carries 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 (flex `stretch`), so give figure images explicit dimensions —
  which they need anyway to avoid layout shift.

#### Reboot

- **Links change color on hover.** `--cui-link-hover-color` was 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 `.2em` below the text.** `<a>` reads the new
  `--cui-link-underline-offset` token (knob: `$link-underline-offset`), so
  underlines clear descenders instead of cutting through them. The
  `.underline-offset-*` utilities and `.icon-link` still override it per element;
  set the token to `auto` for 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`, `email` and `number` inputs 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.
- `pre` reserves 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`, `mark` padding, the code font size and `--cui-list-padding-x` are
  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-radius`
  included.
- **`.h1`–`.h6`, `.small`, `.mark` and `.initialism` are defined in Reboot**,
  next to the elements they mirror, instead of `@extend`-ing them from
  `_type.scss`. Only `.initialism` changes cascade layer (`content` → `reboot`);
  the others were already emitted there, because `@extend` hoists a class to
  wherever the extended selector is defined. `_type.scss` is gone with
  `.lead` and `.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-floating` matches 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-plaintext`
  keeps it hidden.
- **New: `.form-floating-always`** keeps the label floated and the placeholder
  showing while the field is empty. It works on `.form-floating` or on any
  ancestor. See [Always floating](https://coreui.io/bootstrap/docs/forms/floating-labels/#always-floating).

#### Lists

- **`--cui-list-inline-padding` is new.** The gap between `.list-inline-item`s was
  compiled into the rule, so respacing an inline list meant rebuilding from Sass.
  `.list-inline` declares 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 a `data-cell` attribute for its label; the
  header is hidden visually and kept in the accessibility tree. See
  [Stacked tables](https://coreui.io/bootstrap/docs/content/tables/#stacked-tables).
- **New: `.table-card`** wraps 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](https://coreui.io/bootstrap/docs/content/tables/#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-width` and
  `--cui-table-group-separator-color` are declared on `.table` instead of being
  compiled into the cell rule. This also fixes stacked tables, which read
  `--cui-table-cell-padding-*` and `--cui-table-border-width` while nothing
  declared them, so their rows collapsed to zero padding and lost the separating
  line.
- **`.table-sm` sets 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`/`-x`
  instead.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The contrast factors are percentages.** `$table-striped-bg-factor`,
  `$table-active-bg-factor` and `$table-hover-bg-factor` changed from unit-less
  fractions (`.05`) to percentages (`5%`), because they now ship as the
  `--cui-table-*-bg-factor` variables that `color-mix()` reads at runtime.
- **The state colours follow `--cui-table-color`.** `--cui-table-striped-color`,
  `--cui-table-active-color` and `--cui-table-hover-color` default to
  `var(--cui-table-color)`, and the matching backgrounds are mixed from it, so a
  variant only has to set `--cui-table-color`, `--cui-table-bg` and
  `--cui-table-border-color`.

#### Blockquote

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The source line reads `--cui-secondary-color`.** `.blockquote-footer` was
  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-gap` rather 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-footer` drops its own bottom margin instead
  (`:has()`), so the two spacings can be set independently:
  `--cui-blockquote-margin-y` and `--cui-blockquote-footer-margin-y`.

#### Toast

- **New: `Toaster`** creates 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-container` placed by `placement`, caps visible toasts with
  `limit` (the oldest stay in the DOM, `inert` and hidden), announces each
  toast through `announce()` by `priority` and can render an action button. Toasts slide in from beyond the container's edge and back
  out, toasts over the `limit` fade out instead of vanishing, and the toaster
  glides the remaining toasts into place when one leaves; `stack: true`
  piles 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 the `position-fixed top-0 end-0`
  utility stacks. New classes: `.toast-title`, `.toast-description`,
  `.toast-action`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The fade moved to CSS.** Toast was the last component driving its animation
  from JavaScript — it toggled `.fade`, forced a reflow, then juggled `.show`,
  `.showing` and a deprecated `.hide` class to fake an exit. Menu and the picker
  popup already express this with `@starting-style` and a discrete `display`
  transition, and Toast now does the same. JavaScript only toggles `.show`.
  - The `animation` option and `data-coreui-animation` are gone. Add
    `.toast-instant` to skip the animation.
  - `.showing` and the deprecated `.hide` class are gone, and toasts no longer
    get the generic `.fade` class. Replace any CSS or tests that depend on them.
  - The transition is tuned with `--cui-toast-transition-duration` and
    `--cui-toast-transition-timing`.
  - `isShown()` returns `false` as soon as `hide()` is called, rather than when
    the fade-out ends.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Toasts are opaque by default.** `--cui-toast-bg` and `--cui-toast-header-bg`
  resolved to `color-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 to
  `var(--cui-body-bg)`. Add `.toast-translucent` for the see-through look, which
  puts `blur(5px) saturate(180%)` behind the toast and thins the header to match,
  the same way `.card-translucent` and `.menu-translucent` work.
- **New: `.toast-container-{top|middle|bottom}-{start|center|end}`.** Nine
  classes pin a `.toast-container` to 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 the `position-fixed top-0 end-0
  p-3` utility stack; inside a positioned box add `.position-absolute` to pin
  the container to the box. `--cui-toast-container-inset` is declared on
  `.toast-container` next to `--cui-toast-zindex`.

#### Card

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A card is roomier and framed in the solid border colour.**
  `--cui-card-spacer-y` / `-x` go from `1rem` to `--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-x` as their
  inline padding, so their text lines up with the body; the header and footer
  keep `.5rem` of block padding. The border reads `--cui-border-color` instead
  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-y` and `-x` to `var(--cui-spacer-5)` on `.card`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.card-header-pills` is 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-contrast` with `--cui-card-cap-color` as 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-height` and `--cui-card-box-shadow` shipped as empty declarations
  (`--cui-card-title-color: ;`), because their Sass variables were `null` and
  interpolating `null` yields an empty string rather than dropping the entry.
  An empty custom property makes every `var()` 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 ship `inherit`, `auto` and `none`, which compute to
  the same thing and make the customization surface inspectable.
- **The last card in a `.card-group` gets 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-base` was 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 `.card` itself behaves exactly as before.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.card-body` is a flex column.** It spaces its children with
  `--cui-card-body-gap` and 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-100` where a block child has to stretch), and `text-align` no longer moves
  a button or a link, because they are flex items — use `.mx-auto` or `.ms-auto`
  instead.
- **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 `hidden` or `.d-none` does not count. A header and a footer draw their
  own divider, as before.
- **`.card-group` squares 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.card-link` is 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-row`** lays a card out along the inline axis, with
  `.card-img-start` and `.card-img-end` for 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-subtle`** swaps the theme's solid fill for its subtle
  background, border and emphasis text.
- **New: `.card-translucent`** blurs 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`).
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.card-group` attaches its cards from a container query, not a media
  query.** The `sm` breakpoint 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`](https://coreui.io/bootstrap/docs/utilities/container-queries/); without a container the
  query has nothing to read and the cards stay stacked at every width.

#### List group

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.list-group-horizontal-{sm|md|lg|xl|2xl}` turns horizontal from a
  container query.** Same change and same fix as `.card-group`: put
  [`.contains-inline`](https://coreui.io/bootstrap/docs/utilities/container-queries/) on an ancestor, or the
  list stays vertical. Plain `.list-group-horizontal` is unconditional and
  needs no container.
- **An active item under a `.theme-*` class is filled, like a page link.**
  `.list-group-item.active` reads `--cui-theme-contrast` for its text and
  `--cui-theme-bg` for its background and border — the same slots the
  pagination, the badge and the button use — so the active item of a
  `.theme-danger` list is the same red as the active page of a
  `.theme-danger` pagination. A pressed action (`:active`) reads
  `--cui-theme-bg-muted`, like hover. Without a theme class nothing changes.

#### Tab

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A tab pane fades on its own — `.fade` on a `.tab-pane` does nothing for
  it.** `.tab-pane` carries the transition and
  `--cui-tab-pane-transition-property` / `-duration` / `-timing`, and
  `@starting-style` supplies the state to fade from. Drop `.fade` from your
  panes; add `.tab-pane-instant` to a pane that should switch without one.
  `.fade` itself stays as a generic helper for your own markup.

- **The pane no longer waits a full transition before it starts fading.** The
  JS added `.show` from 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, and `shown` waits on the pane, the
  element that actually animates, instead of the tab button, which only
  transitions its colours.

#### Modal

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A size modifier applies at every viewport.** `.modal-lg` and `.modal-xl`
  were withheld until the `lg` and `xl` breakpoints, 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 by `max-width: calc(100% - margin * 2)` exactly as
  before, which is what kept horizontal scrollbars away in the first place.
  At 768 px `.modal-lg` goes 500 → 712 px, and at 1100 px `.modal-xl` goes
  800 → 1044 px.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.modal-sm` is narrow on phones too.** It used to fall back to the default
  width below the `sm` breakpoint, 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-sizes` replaces 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A page link is sized from the shared control scale.** Pagination kept its
  own padding values, so `.pagination-lg` did not match `.btn-lg` or
  `.form-control-lg` — it measured **56 px** against their 48. The tokens read
  `--cui-btn-input-*` now, `.page-link` is 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.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`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-visible` read
  `--cui-pagination-focus-color` and `-focus-bg` with no theme slot, so tabbing
  to a link inside `.theme-success` dropped it back to the plain link colour.
  Both now read `--cui-theme-fg-emphasis` / `--cui-theme-bg-subtle` first, like
  every neighbouring state.

- **`$pagination-margin-start` is removed.** The overlap between links is
  `calc(-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-max` and
  `--cui-placeholder-opacity-min` on `.placeholder` set the resting opacity
  and the pulse, and `--cui-placeholder-wave-opacity-max` and `-min` on
  `.placeholder-wave` set its highlight, so both can be retuned on an
  instance. They were compiled in from `$placeholder-opacity-max` and
  `-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 is
  `1.25em` (was `1em`), capped a quarter em below the line height so a
  placeholder never makes a heading or a tight line taller; `.placeholder-xs`
  is `.75em` (was `.6em`), `.placeholder-sm` `1em` (was `.8em`) and
  `.placeholder-lg` `1.75em` (was `1.2em`). Each placeholder has a
  `--cui-radius-3` corner. A button, avatar, badge or chip that takes
  `.placeholder` as a modifier keeps its own height and corners.
- **`.placeholder-wave` is 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-pulse` now; `.placeholder-glow` stays as its alias.
  The keyframes are renamed with it, so custom CSS that names
  `animation: placeholder-glow` needs `placeholder-pulse`.
  With reduced motion the wave stops and drops its mask.

#### Popover and Tooltip

- <span class="badge fg-emphasis-warning bg-subtle-warning">Deprecated</span> **`placement`
  takes the logical `start` and `end`; `left` and `right` are deprecated.**
  The physical names never meant a physical side — both components have always
  mirrored them in RTL, which their own pages documented — so `right` was
  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: `left` and `right` keep working and resolve identically, and they are
  removed in v7.
- **The fade moved to CSS.** Neither component renders the `fade` helper class
  any more; `.show` alone drives the transition, with `@starting-style`
  supplying the state to animate from. `animation: false` renders
  `.popover-instant` / `.tooltip-instant` instead of omitting `fade`, and a
  custom `template` opts out with the same class. The option itself is
  unchanged; only custom CSS targeting `.popover.fade` or `.tooltip.fade` needs
  updating. Tune the motion with `--cui-popover-transition-duration` /
  `-timing` and 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-bg` when a header follows it, and stays on
  `--cui-popover-bg` when there is none.

- **`--cui-popover-header-color` reads `--cui-heading-color`.** It resolved
  `$headings-color` at build time, so a runtime override of the heading colour
  skipped the popover header. The default is unchanged.

#### Progress

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$progress-bar-animation-timing` is 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-size` to go
  back.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.progress-group`, `.progress-group-prepend`, `.progress-group-header` and
  `.progress-group-bars` are 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` / `.vstack` with a `gap-*` and put the
  label column's width in a style, as the Grouping bars examples now show.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.progress-thin` is removed.** It did nothing an inline height does not. Use
  `style="height: 4px"` on the `.progress`, as the Height section shows, or set
  `--cui-progress-height` if you want the stripe geometry to follow the height
  too.

- **`.progress-white` sets tokens.** It was painting `background-color` past
  `--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` (default `none`) paints the
  bar with an image on top of its colour, so a gradient is one variable on the
  `.progress` or the bar. The stripes moved to their own tokens for the same
  reason: `--cui-progress-bar-stripes` holds the pattern and
  `--cui-progress-bar-stripe-color` its colour, and `.progress-bar-striped`
  layers the pattern over the image. The `progress-bar-stripes` keyframes now
  shift only the first background layer, so the image underneath stays put.

- **`.progress-segmented`.** A mask that cuts a `.progress` or
  `.progress-stacked` into a row of rounded segments, `--cui-progress-segment-min-width`
  wide and `--cui-progress-segment-gap` apart — or exactly `--cui-progress-segments`
  of them when that is set — with corners from
  `--cui-progress-segment-border-radius` (`--cui-radius-2` by default, `0px`
  for square). Everything under the mask — transition, theme,
  gradient, stripes — behaves as before.

- **New `Progress` plugin.** Initialized on every `.progress-segmented`, it
  reads the bar's `aria-valuenow`, `aria-valuemin` and `aria-valuemax` and 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-width` fit the track, capped at
  `--cui-progress-segments` when set, and follows the track as it resizes; a
  script that changes the bar's `aria-valuenow`, `aria-valuemin` or
  `aria-valuemax` calls `update()`. An inline `width` on the bar still wins
  over the segment fill.
  `coreui.Progress` is exported next to the other plugins.

#### Spinner

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Spinners are sized in `em`, not `rem`.** `.spinner-border` and
  `.spinner-grow` are now `1em` square with a `.125em` border, 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-height` Sass variables, to go back to a fixed size.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.spinner-border-sm` and `.spinner-grow-sm` are replaced by one
  `.spinner-sm`.** The small variant no longer has to correct the border width,
  so a single class serves both spinners.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-spinner-vertical-align` is gone.** Spinners are
  `inline-flex` and assume a flex parent; the `-.125em` baseline nudge it used
  to need no longer applies.

- **The animation timing function is a token.** `.spinner-border` keeps
  `linear`, while `.spinner-grow` now eases with `ease-in-out` instead of
  pulsing linearly. Override
  `--cui-spinner-animation-timing-function` to change either.

- **`.spinner-rotate` is new.** It applies only the rotation, so you can spin
  your own SVG or icon without inheriting the bordered circle.

- **`.btn-loading` sizes its spinner from the button.** `LoadingButton` no
  longer adds a `-sm` class to the spinner it injects; the spinner follows the
  button's font size, so it now scales with `.btn-sm` and `.btn-lg`.

#### Footer

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.footer` is 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 is `min-height: 3rem`; where a
  floor matters, set it yourself or pad with `py-4`. The
  `--cui-footer-*` variables and the `$footer-*` Sass variables go with it.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.footer-sticky` is replaced by `.sticky-bottom`.** It pins to the bottom
  the same way but sits on `--cui-z-sticky` rather than `--cui-z-fixed`, so a
  footer that has to stack above fixed chrome needs its own `z-index`.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.footer-fixed` is 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.header-sticky` is 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-fixed` never existed.** The `position` property on the framework
  components accepted `fixed` and rendered a class the stylesheet did not
  define, so it did nothing. Use `.fixed-top` or `.fixed-bottom`; on the
  framework components, `position` renders those utilities now.
- **`.prevent-hide` on `.header-toggler` is removed.** It forced
  `display: block !important` against 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-subtle` next
  to it fills the callout without naming the colour twice.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Deprecated</span>
  **`.callout-{color}` still works and is removed in v7.** Each modifier is
  generated from `$theme-colors` through the same `theme-tokens()` mixin as the
  theme class, so `.callout-primary` *is* `.callout.theme-primary`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$callout-variants` is 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-color` was read but never
  declared: only the modifiers set it, so the declaration was invalid on a bare
  `.callout` and 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-border` where 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 in `coreui-legacy.css`.**
  Load it after `coreui.css` and 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-primary` moves whole rather than surviving as a rule with no colour.
- **`alert-modifiers` and `btn-variant-loops` each 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 is
  `alert-variant-loop`, and the deprecated button classes are
  `btn-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:

  ```html
  <!-- 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 `.underline` to carry it; its colour now defaults to
  `currentColor`, so it follows the theme unless you name one with
  `.underline-{color}`.

  `.link-body-emphasis` becomes `.text-body-emphasis`.

  The removed classes also computed their `:hover` colour in Sass, which froze it
  at build time — overriding `--cui-primary` at runtime never reached the hover
  state. The theme slots are tokens, so it does.

- **`.ratio` is now a native aspect-ratio utility.** The helper built its ratio from a
  `::before` with a percentage `padding-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 CSS `aspect-ratio` property on the
  element itself.

  ```html
  <!-- v5 -->
  <div class="ratio ratio-16x9">
    <iframe src="..."></iframe>
  </div>

  <!-- v6 -->
  <iframe class="w-100 ratio-16x9" src="..."></iframe>
  ```

  The wrapper and the bare `.ratio` class 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-ratios` map moved from `helpers/_ratio.scss` to `_config.scss` and 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-danger` set, 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 `$…-tokens` map, 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-solid` and the other button variants, `.alert-solid`, `.badge-subtle`,
  `.badge-outline`, `.avatar-subtle` and `.chip-outline` map 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Components read stops of a radius scale, and most got rounder.** `:root`
  emits `--cui-radius` (`$radius`, `.5rem` by default) and the stops
  `--cui-radius-0` through `--cui-radius-9` as `calc()` 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 fixed `3rem`, which
  keeps a pill up to 96px tall fully round and does not follow `--cui-radius`.
  Everything that used the `.375rem` default 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-radius` is gone from `:root` as well: nothing in the library
  read it, so a stylesheet setting it changed nothing — set `--cui-radius` or
  a stop instead.

#### Interaction surfaces sit two rungs lighter on the ladder

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The neutral button steps down the ladder with everything else.** A `.btn`
  without a theme colour rests on `--cui-bg-2` (was `--cui-bg-4`) and hovers
  on `--cui-bg-3` (was a hard-coded `gray-300`/`gray-700` pair outside the
  ladder); the neutral subtle variant rests on `--cui-bg-2` and 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-group`
  clips to the same stop. Set `$btn-bg`/`$btn-hover-bg` or the `--cui-btn-*`
  tokens to keep the old look.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-3` instead 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-2`
  and `--cui-bg-3` like the native range. The card header and footer read `--cui-bg-1` instead of a
  3 % foreground wash, so a card on a tinted parent keeps a neutral cap.
  The tab's hover border reads `--cui-border-subtle` rather than a surface
  rung.
- **Foreground roles line up too.** The disabled calendar cell and the
  disabled combobox option read `--cui-fg-4` / `--cui-fg-3` like every other
  disabled control, the weekday header and the date-input placeholder read
  `--cui-fg-3` like every other caption and placeholder, and the active
  breadcrumb reads `--cui-fg-1`, one rung above the hovered link instead of
  the same one.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Border tokens move one rung towards the page.** `--cui-border-color` is
  `light-dark(gray-200, gray-700)` (was `gray-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-body` to read as a
  line. `--cui-border-subtle` is a half step, mixed from `gray-100` and
  `gray-200` (`gray-800` / `gray-900` in dark) — plain `gray-100` had become
  invisible against `--cui-bg-1` on the spread-out light band.
  `--cui-border-emphasized` is `gray-600` in the dark scheme (was
  `gray-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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$theme-bgs`, `$theme-fgs` and `$theme-borders` replace the neutral
  scalars.** `$fg-body`, `$fg-1` … `$fg-4`, `$bg-body`, `$bg-1` … `$bg-4`,
  `$border-subtle`, `$border-muted` and `$border-emphasized` are gone; each
  map lives in `scss/_theme.scss` in the `defaults()` shape, `:root` emits
  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-color` stays a scalar — it is the default every
  component reads, not a rung. Override an entry instead of a variable:

  ```diff
  - $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-white` and
  `.border-black` read tokens like every other rung. `.border-body` reads
  `--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-color` is `light-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-color` is 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-color` and the hover follows.
- **`--cui-hr-border-color` is `var(--cui-border-color)`** (was
  `currentcolor`): a rule is the same line as every other border, not a
  faint copy of the text colour.

#### Theme colours have no bare token

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-primary`, `--cui-secondary`, `--cui-success`, `--cui-info`,
  `--cui-warning`, `--cui-danger` and `--cui-inverse` are 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 `-base` slot: `--cui-primary-base` is what
  `--cui-primary` was, 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:

  ```diff
  - .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 `-base` now; the derived
  hover and active states of a `.theme-*` class are computed from `-bg`.

#### Breakpoints: `lg` 1024, `xl` 1280, `2xl` 1536

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The three largest breakpoints move and the largest is renamed.** `lg` is
  `1024px` (was 992), `xl` `1280px` (was 1200) and the top tier is `2xl` at
  `1536px` (was `xxl` at 1400); `sm` and `md` stay. Every responsive class
  spells the tier the new way — `.col-xxl-6` is `.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: `.container` is
  `1200px` at `xl` (was 1140) and `1440px` at `2xl` (was 1320). Only
  `.icon-xxl` keeps 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-widths` keeps 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 below `1024px` — a tablet
  in landscape gets the mobile layout.

#### Fixed widths and a vertical gutter knob

- **New: `.w-1` … `.w-12`**, one `$spacer` per step from the `$sizes` map,
  merged into the width utility next to the percentages.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$grid-gutter-width` is `$grid-gutter-x`, and `$grid-gutter-y` joins 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 to `0` as
  before, so nothing moves unless you set it. `make-grid-columns()` and the
  card group read `$grid-gutter-x`. `$grid-gutter-width` stays as a deprecated
  alias of `$grid-gutter-x` until 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 as `dropdown`, `offcanvas` and `modal` (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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$font-sizes` sets a line height per stop instead of one value for all
  headings.** `xs` is 1.25, `sm` … `lg` the body leading (`--cui-body-line-height`, 1.5), `xl` 2.5⁄1.75, `2xl` 3⁄2.25,
  `3xl` 1.2, `4xl` and `5xl` 1.1, `6xl` 1 — 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`) and `h4` (`xl`) get taller lines, `h1`
  (`4xl`) a tighter one. `$headings-line-height` still feeds
  `--cui-heading-line-height` for anything that reads it directly.

#### Layered shadows

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-xs` and `--cui-box-shadow-xs` are new (one thin layer).
  The inset shadow reads 8 % in the light scheme and 26 % in the dark one.
  `--cui-shadow-strength` in the dark scheme is `2.4` (was 2.5). The knobs are
  unchanged: `--cui-shadow-color` and `--cui-shadow-strength` on `:root`,
  `.shadow-{color}` and `.shadow-opacity-*` on the element, `$shadows` in
  Sass — a custom entry is now a comma-separated list of layers.

#### A denser spacing scale

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$spacers` has thirteen steps, `0`–`12`.** From `1` the 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` | `.25rem` | `1` |
  | `2` | `.5rem` | `3` |
  | `3` | `1rem` | `5` |
  | `4` | `1.5rem` | `7` |
  | `5` | `3rem` | `12` |

  `.p-3` becomes `.p-5`, `.mb-4` becomes `.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](#card).

#### Padding tokens carry both axes

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **A token that sets `padding` names an axis.** The six that v5 also had as
  custom properties are `-padding-y` and `-padding-x` now:
  `--cui-modal-padding`, `--cui-modal-header-padding`,
  `--cui-calendar-nav-padding`, `--cui-nav-enclosed-padding`,
  `--cui-date-picker-footer-padding`, `--cui-time-picker-footer-padding` and
  `--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](#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-padding` is 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-padding` is the space between `.list-inline-item`s and
  sets a margin.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-modal-header-padding` is 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-y` and `-padding-x`, defaulting to
  `--cui-modal-padding-y` / `-padding-x`.
- **A distance between children is a `-gap` token.** `--cui-date-picker-ranges-gap`,
  `--cui-date-picker-footer-gap`, `--cui-time-picker-footer-gap`,
  `--cui-drawer-body-gap` and `--cui-drawer-footer-gap` are new; the time
  picker's footer used to space its buttons with a `.btn + .btn` margin,
  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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$link-decoration`, `$link-underline-offset` and `$link-hover-decoration`
  are gone.** The underline lives on `:root` as `--cui-link-decoration`
  (`underline`) and `--cui-link-underline-offset` (`.2em`); set the token
  instead of recompiling. `--cui-link-hover-decoration` is 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 write `text-decoration: none`
  outright (they used to skip it when the knob was already `none`), and
  `.btn-link` reads 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 `-bg` slot, not its base.**
  `--cui-theme-bg` is `var(--cui-danger-bg)` now (was `var(--cui-danger)`),
  so overriding `--cui-danger-bg` retunes every checked, active and filled
  state under the class, as the colour slots page describes. The trade-off:
  `--cui-danger-bg` resolves on `:root`, so a `--cui-danger` override on a
  single element no longer reaches the theme class on it — override the
  colour on `:root`, or set `--cui-danger-bg` on 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 `contrast` slot holds written values** instead of
  `color-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.**
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  `.table-primary` and friends still exist, but they are generated the way
  `.btn-primary` is: the class sets `--cui-theme-*` and the base `.table`
  rules read it. `$table-variants` is now a **list of colour names**, not a map
  of pre-shifted colours, and `$table-bg-scale` / `$table-border-factor` are
  gone. A theme colour you add yourself now gets a table variant without
  compiling Sass.
  Two visible consequences: the variant background is the colour's
  `bg-subtle` token rather than a `shift-color()` of the base, and striped /
  hover / active are `color-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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
**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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

- **Every `--cui-{name}-rgb` custom property is gone**, along with the
  `$theme-colors-rgb` and `$theme-colors-rgb-dark` maps, the `to-rgb()`
  function's role in `:root`, and the `rgba-css-var()` and
  `translucent-css-var()` helpers (`color-var-ref()` is the function that
  stays — see the utilities section). They existed only so
  `rgba(var(--cui-primary-rgb), .5)` could apply an alpha to a token; that is
  what `color-mix()` does directly:

  ```diff
  - 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 through `calc(var(--cui-bg-opacity) * 100%)`.
- **The elevation system is gone**: `$elevations`, `$enable-elevations`,
  `$elevation-base-color`, the `elevation()` 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 set `box-shadow` directly.
- **The button focus ring is the theme ring.** `--cui-btn-focus-shadow-rgb` and
  the per-variant `$btn-*-focus-shadow-rgb` Sass 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Controls 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`$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.

```diff
  $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-color` and
  `-invalid-border-color` are 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 `fg` slot 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-opacity` is removed.** The validation ring comes
  from the colour's `focus-ring` slot, which reads
  `--cui-focus-ring-opacity`; set that instead.
- **`$focus-ring` is removed and `$focus-ring-color` reads the slot.** The
  colour was `rgba($primary, $focus-ring-opacity)`, resolved at build time, so
  neither retinting `--cui-primary` nor changing
  `--cui-focus-ring-opacity` at runtime moved the default ring. It is
  `var(--cui-primary-focus-ring)` now — the slot every `.theme-*` ring
  already mixed from. `$focus-ring` itself only restated
  `--cui-focus-ring`, which `_root.scss` composes, so the two variables that
  read it — `$btn-close-focus-shadow` and `$sidebar-toggler-focus-shadow` —
  are gone with it; the close button and the sidebar toggler draw their ring
  with the shared `focus-ring()` mixin.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-sidebar-toggler-focus-shadow` is removed.** It fed a `box-shadow`
  on `.sidebar-toggler:focus-visible` next to an `outline: 0`, and its value
  was the `--cui-focus-ring` shorthand — `<width> solid <color>`, which
  `box-shadow` does not accept. The declaration was dropped as invalid and
  the toggler had no focus indicator at all. The ring is an `outline` now,
  like everywhere else; restyle it through `--cui-focus-ring-width`,
  `--cui-focus-ring-color` and `--cui-focus-ring-offset`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`add()` and `subtract()` are removed.** They wrapped `calc()` to keep a
  unitless `0` from making the expression invalid. v6 computes derived values
  in the browser, so the stylesheet reaches for `calc()` directly — 243 times,
  and not once for these two. Write the `calc()` yourself; guard a possible
  `0` with `if($value == 0, …)` where it can actually occur. `divide()` stays,
  the grid still uses it.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The theme colour tokens finish the rename to the slot names.**
  `--cui-{color}-text-emphasis` is `--cui-{color}-fg-emphasis` and
  `--cui-{color}-border-subtle` is `--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}-base` is 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-text` and `$theme-colors-border-subtle`
  are gone with the other per-slot maps, and `theme-color-refs("fg-emphasis")`
  returns the slot's token for every colour. **No class names
  change** — `.text-primary-emphasis` and `.border-primary-subtle` take their
  suffix from the utility definition, not from the token.
- **`form-validation-state()` takes `$theme` where it took `$color`**, and the
  variables moved from `scss/_config.scss` into the partials that use them
  alongside `$form-text-*` and `$form-feedback-*`.
- **The validation tooltip is opaque and sits on the state's `-bg` slot.**
  It was `color-mix(in srgb, var(--cui-danger) 90%, transparent)`, with the
  percentage frozen at build time from `$form-feedback-tooltip-opacity`. It
  reads `var(--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-opacity` is removed; for a translucent
  tooltip set `background-color` on `.invalid-tooltip` / `.valid-tooltip`.

#### `$component-active-color` and `$component-active-bg` are removed

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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.

```diff
- $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-color` was
  `rgba($white, .87)`, a colour frozen at build time, so an active state
  recoloured to a light background kept near-white text. It reads the
  `contrast` slot 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-500` at 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-color` in every state, so `$switch-thumb-bg` and
  `$switch-checked-thumb-bg` are ordinary colour knobs; the v5 knob images and
  the variables that fed them (`$form-switch-bg-image` and friends) are gone.

#### A theme colour is one nested map entry

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`$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-emphasis` and its
  seven siblings, `$primary-bg-subtle` and its seven, `$primary-border-subtle`
  and its seven. Override a slot through the map instead:

  ```diff
  - $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.scss` and change the slot. `$theme-colors`
  goes through `defaults()` like every other configuration map, so a new colour
  is one entry passed through `with ()` and `null` removes one.

  Every `--cui-{color}-*` custom property keeps its name and its value, so
  a runtime override is unaffected.
- **`$theme-colors` is a map of maps**, so `@each $name, $value in $theme-colors`
  now yields a map. Use `map-get-nested($theme-colors, "base")` where a flat
  `name → value` map is what you want, or `theme-color-refs("base")` for the
  `:root` tokens the utilities read.
- **New slots:** `fg` (the state colour readable as text, one step lighter than
  `text-emphasis`), `bg-muted` (between `bg-subtle` and the solid colour) and
  `focus-ring` (the hue the `.theme-*` ring mixes from). They emit
  `--cui-{color}-fg`, `-bg-muted` and `-focus-ring`, and reach components
  as `--cui-theme-fg`, `--cui-theme-bg-muted` and
  `--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.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$input-focus-border-color` is a `var()` reference, not a Sass colour.**
  It was `tint-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-color` now (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.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$time-picker-roll-cell-selected-hover-bg` is computed in the browser.**
  It was a `light-dark()` pair of `color.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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`--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:

```css
: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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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:

```css
: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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`$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:

```scss
// before
@use "@coreui/coreui-pro" with ($input-padding-y: 1rem);

// after — Sass
@use "@coreui/coreui-pro" with ($input-btn-padding-y: 1rem);
```

```css
/* 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`$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 `@use` the 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, and `buttons/_button.scss` reads them.
- **`$input-height`, `$input-height-sm` and `$input-height-lg` are removed.**
  They existed to be baked into `--cui-btn-input-min-height`, which now
  computes from `--cui-btn-input-line-height`, `-padding-y` and
  `-border-width` in the browser. Set those tokens, or
  `--cui-btn-input-min-height` itself, instead of recompiling.

#### Shadows are computed at runtime

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

A 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-lg` and `$box-shadow-inset`
  are replaced by the `$shadows` map.** 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:

  ```diff
  - $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-dark`** and the
  `--cui-shadow-color` / `--cui-shadow-strength` tokens 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 plus
  `current`/`black`/`white`, and `.shadow-opacity-{25|50|75|100}`. They write
  `--cui-shadow-tint` and `--cui-shadow-opacity`, both registered
  `inherits: false`, so a tinted element does not recolour the shadows nested
  inside it.
- **Dark mode raises `--cui-shadow-strength` to 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

- **`.bg-{color}-gradient` and `.text-bg-{color}-gradient` are gone**, along
  with the `$theme-gradients` / `$theme-gradients-dark` maps that generated
  them, the per-colour `$primary-gradient` … `$dark-gradient` variables (and
  their `-dark` twins), and the `--cui-{color}-start` / `--cui-{color}-stop`
  custom 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-gradient` itself stays**, as do `$gradient`, `--cui-gradient`,
  `$enable-gradients` and the `gradient-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 in `colors`, so your own `@layer colors` block overrides a
  token like `--cui-blue-500` by source order alone. Two of the layers are empty
  and reserved for you: `config`, below `root`, for a configuration sheet the
  library should read but never fight, and `custom`, 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`.**
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  The `$enable-important-utilities` switch 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 custom` so 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>-tokens` Sass map** merged over your overrides with the new
  `defaults()` function. Override the whole surface at once:

  ```scss
  @use "@coreui/coreui-pro/scss/coreui" with (
    $alert-tokens: (
      --cui-alert-bg: hotpink,
      --cui-alert-margin-bottom: null, // null removes the token entirely
    )
  );
  ```

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-bg` on 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 variables` section entirely.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`scss/_root.scss` declares no variables of its own any more** — 62 went,
  leaving only `$root-tokens` for 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-tokens` when you
  compile, or on `:root` at runtime:

  ```scss
  @use "@coreui/coreui-pro/scss/coreui" with (
    $root-tokens: (
      --cui-body-line-height: 1.6,
      --cui-btn-input-padding-y: .5rem,
    )
  );
  ```
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **Sixteen more page-wide knobs are gone.** `scss/_theme.scss` declares 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-lightness` and
  `--cui-theme-active-chroma`, and the theme page documents them. From
  `scss/_config.scss`, `$gradient`, `$border-style`, `$font-family-base`,
  `$headings-color` and `$link-color` go the same way, to `--cui-gradient`,
  `--cui-border-style`, `--cui-body-font-family`, `--cui-heading-color` and
  `--cui-link-color`. All seven `$font-weight-*` scalars are folded
  into `$font-weights`, which stays the way to add or drop a weight. `dt` and the headings read
  `--cui-font-weight-bold` and `--cui-font-weight-medium` instead of a number
  frozen at build time, so retuning a weight on `:root` now moves them too. `$headings-line-height` goes to `--cui-heading-line-height`, `$font-size-base` to `--cui-body-font-size`, and `$font-size-root`
  goes entirely: it gated a `font-size` on the root element at build time and
  defaulted to `null`, so it emitted nothing. Set `font-size` on `html` in your
  own CSS to scale `rem` instead. `$border-width` and `$border-color` go the
  same way, to `--cui-border-width` and `--cui-border-color`; the width scale and the `.border-{key}` classes
  read the token, not the variable. `$blue` and `$pink` are finally gone from
  the source, which is what the palette entry above already said.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-3` and `--cui-fg-3`, and
  `code` reads `--cui-font-mono`.
- **`mark` carries its own tokens.** `--cui-mark-padding-y`/`-padding-x`,
  `--cui-mark-color` and `--cui-mark-bg` were declared on `:root`, where
  nothing else read them.
  They sit on `mark, .mark` now, in a `$reboot-mark-tokens` map you can override
  like any other component surface. Setting them on `:root` no longer reaches
  the element; set them on the element, or on the map when you compile.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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`.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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 a `background-image` on 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$radius` and `$chip-gap` are gone**, to `--cui-radius` and
  `--cui-chip-gap`. The radius scale still multiplies one value, but that value
  is the token: set `--cui-radius` and every stop follows at runtime.
- **`$font-weight-semibold` and `$grid-gutter-width` are gone too.** Both were
  kept only because the shared docs engine read them; it reads
  `--cui-font-weight-semibold` and `$grid-gutter-x` now. The weight lives in
  `$font-weights`, and `$grid-gutter-x` is the gutter.
- **What stays in `scss/_config.scss` stays 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-colors` stays for the same
  reason, in `scss/_theme.scss`. So does every component variable a rule or a mixin
  still reads.
- **Component Sass variables moved from `_config.scss` into their component
  partials.** `@use ... with()` is unaffected — the entry point forwards every
  module — but if you imported `scss/_config.scss` directly for its
  definitions, component blocks are no longer there.
- Two silent duplicates were dropped along the way: `$nav-link-height` was
  declared twice (navbar and header blocks — the header copy is gone) and the
  calendar emitted `--cui-calendar-cell-week-number-color` twice.
- **Time picker: the v1-only tokens are gone.** The rewritten shell draws its
  adornments as inline SVG on `currentColor` and styles them through
  [`.form-control-action` / `.form-control-cleaner`](https://coreui.io/bootstrap/docs/forms/form-control-group/),
  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-gap` and `--cui-time-picker-placeholder-color`, together
  with their Sass variables (`$time-picker-cleaner-icon-size-sm`/`-lg` were
  also declared twice). `--cui-time-picker-indicator-icon-color` stays 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-invalid` with
  `!important` overrides, v2 reads it from the field inside the group, so
  `$time-picker-invalid-border-color` / `$time-picker-valid-border-color` and
  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 own `hourPlaceholder` /
  `minutePlaceholder` / `secondPlaceholder` options, not a colour token.
- `$input-padding-x-lg` was removed: `.form-control-lg` reads
  `--cui-btn-input-lg-padding-x` from the shared button/input token system, and
  the time picker's dead `gap-lg` was the variable's last consumer.
  `$input-padding-x`, `$input-padding-x-sm` and `$input-padding-y-*` are
  unaffected.

#### Theme tokens

- New `--cui-theme-*` indirection plus generated `.theme-primary` …
  `.theme-inverse` classes: 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-success` are now
  implemented through these tokens — same rendering, new mechanics.
- **`--cui-theme-focus-ring` joins 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 as
  `var(--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}-contrast` custom properties are new root tokens.** Previously
  only buttons computed their own contrast colour; the root tokens now carry it,
  written in the `contrast` slot. `.text-bg-*` reads the token instead of
  calling `color-contrast()`.

#### `--cui-mobile-breakpoint` removed

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Dark mode no longer has a second set of Sass variables. Every colour is declared
once, as a native
[`light-dark()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark)
pair, and `[data#{$data-infix}theme="dark"]` only declares `color-scheme: dark`.

- **`scss/_variables-dark.scss` is gone.** All 59 `$*-dark` variables it held are
  gone with it; do not `@import "variables-dark"` any more. The values live
  inline in the light variable:

  ```diff
  - $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 its `500` stop: `--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-500` at runtime and the whole scale retints.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The palette is the theme colours.** `$colors` holds `primary`, `success`,
  `info`, `warning`, `danger` and `gray`; `$blue`, `$indigo`, `$purple`,
  `$pink`, `$red`, `$orange`, `$yellow`, `$green`, `$teal`, `$cyan` and 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.
  `$secondary` and `$inverse` are gone too — they read gray stops, not a
  colour of their own. Each `$theme-colors` entry points `base` and `bg` at
  its palette's `500` stop (`primary` at `var(--cui-primary-500)`) and the
  other slots at that scale; `secondary` reads `--cui-gray-200`, `inverse`
  `--cui-gray-900`, and their `bg` is the `light-dark()` pair that used to be
  their base. No palette token is bare: the literal sits in the `-500` stop,
  so `--cui-gray-500` is what `--cui-gray` was.

  ```diff
  - :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 `$colors` plus an entry in `$theme-colors`
  that points at it; see [adding a theme colour](https://coreui.io/bootstrap/docs/customize/theme/#adding-a-theme-color).
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The palette is written in `oklch()` and mixes in `oklch`.** The base colours
  are unchanged — every hex converts to the same sRGB pixel — but the 100–900
  steps mixed from them shift, because `oklch` holds chroma where `lab` drained
  it (measured: 5.9 per RGB channel on average, 28 at most). `$black` is `#000`
  now rather than a near-black with a blue cast: `oklch` interpolates hue, so
  mixing a scale towards a tinted black rotated every shade towards blue.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The gray ramp is generated like every other colour.** `$gray-025` …
  `$gray-975` were thirteen hand-picked values; they are now one `$gray` base
  run through the same generator, and `gray` is an entry of `$colors`. The Sass
  variables stay, derived from the base, so `$gray-600` keeps working — but the
  ramp is set by `$gray` plus `$color-tints` / `$color-shades`, not by
  overriding a stop.

  `$color-tints` and `$color-shades` gained `025`, `050`, `950` and `975`, so
  **every** colour now carries the full thirteen stops. The curve is
  `94 / 90 / 80 / 60 / 40 / 20 %` of white for `025` … `400` and
  `16 / 32 / 48 / 64 / 70 / 76 %` of black for `600` … `975`, and `$gray` is
  `oklch(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-100` reads 1.27:1 (was 1.10),
  `gray-200` 1.63 (was 1.21), `gray-300` 2.14 (was 1.34), `gray-400` 2.87
  (was 1.49); `gray-600` and below are within a step of where they were.
  Everything that reads a light stop moves with it — the neutral surfaces
  `bg-1` … `bg-4`, `border-color`, `theme-secondary`, every `-bg-subtle` and
  `-border` slot — which is what makes a plain badge or a secondary button
  visible on a white page again.

- **`-bg-subtle`, `-border-subtle` and `-text-emphasis` are pairs of those
  stops** rather than hand-picked hexes:

  ```scss
  $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 `light` and `dark` had genuine dark
  variants before; for the six chromatic colours `$primary-bg-subtle-dark` was an
  alias of the light value, so `.alert-success`, `.list-group-item-info` and
  `.table-warning` rendered 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-dark` and the
  `$grays-dark` map. Every one of them was defined as exactly its light
  counterpart, so they generated nothing — read `$gray-*` directly.
- **`$theme-colors-dark` is gone**, along with `$theme-colors-fg-emphasis-dark`,
  `$theme-colors-bg-subtle-dark`, `$theme-colors-border-dark` and
  `$theme-colors-contrast-dark`, and the eight `$primary-dark` … `$dark-dark`
  variables 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}-dark` root 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; declare `color-scheme: dark` instead and
  the `light-dark()` pairs follow — which is exactly what `.sidebar-dark` now
  does, having dropped its eight token overrides.
- **`$link-shade-percentage` no longer drives `$link-hover-color`**, which is now
  `light-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-weight` and
  `$table-caption-color`. Reboot owns those declarations, so use `$th-font-weight`
  and `$caption-color`.
- Setting `$enable-dark-mode: false` still yields a light-only page: `:root`
  declares `color-scheme: light` and nothing ever declares `dark`, so every
  `light-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:

- <span class="badge fg-emphasis-warning bg-subtle-warning">Deprecated</span>
  **The surface tokens move out of the theme-color namespace.**
  `--cui-bg-body`, `--cui-bg-1` … `--cui-bg-4` and `--cui-fg-body`,
  `--cui-fg-1` … `--cui-fg-4` are 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 the `secondary` theme color alone.
  One alias is already gone:
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-secondary-bg` is the `secondary` theme 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-4`
  instead. 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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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 a `color-mix()` between `gray-800`
  and `gray-900`: the body sits on `gray-975` (was `gray-900`), `bg-1` on
  `gray-950`, `bg-2` on `gray-900`, `bg-3` on `gray-800` and `bg-4` on
  `gray-700`. Dark mode reads darker overall and surfaces have more room to
  step away from the page. `--cui-border-color-translucent` is one expression
  for both schemes, `color-mix(in oklch, var(--cui-fg-body) 15%, transparent)`,
  instead of two `rgba()` values computed in Sass; the `color-translucent()`
  function is gone. Override the `$bg-*` knobs or the `--cui-bg-*` tokens to
  keep the previous palette.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The text ladder is solid grays, and components use all of its rungs.**
  `--cui-fg-body` through `--cui-fg-4` were alpha blends of one color
  (`rgba(…, .95)` down to `.25`), which made the two deepest rungs
  illegible and left every component crowded onto `fg-body`/`fg-2`. Each
  rung is a palette stop now — `fg-body` on `gray-900/050`, `fg-1` on
  `800/200`, `fg-2` on `700/300`, `fg-3` on `600/500`, `fg-4` on `500/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) reads `fg-3` instead of
  `fg-2`, disabled states read `fg-3`/`fg-4` and stay legible, dividers
  read `fg-4`, and the strongest text (tables, active nav links, the
  tooltip surface) reads `fg-body` instead of `--cui-emphasis-color`.
  Nav links are neutral (`fg-2`, darkening through `fg-1` to `fg-body`
  when active) instead of link-colored — a `.theme-*` class still
  recolors them. `$gray-base` fed the alpha blends and is removed.
  Override the `$fg-*` knobs or the `--cui-fg-*` tokens to keep the
  previous rendering.
- **`.text-body` and `.text-body-emphasis` are unaffected** — they are not
  ordinal, any more than `.text-primary` is.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The Sass knobs follow the ladder.** The `$theme-fgs` and `$theme-bgs`
  maps (`"body"`, `"1"` … `"4"`) replace `$body-color`, `$body-bg`,
  `$body-secondary-color`, `$body-secondary-bg`, `$body-tertiary-color` and
  `$body-tertiary-bg`; every entry holds one `light-dark()` expression, so
  `$body-bg-dark` and `$body-tertiary-bg-dark` are gone too — the translucent
  tints that needed their Sass colors read the grays inline.

#### `light` and `dark` are replaced by `inverse`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`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.

- **`inverse` replaces `dark`.** It resolves to `gray-900` in light mode and
  `gray-100` in dark, so `.btn-inverse`, `.text-bg-inverse` and
  `.table-inverse` stand against the page in both.
- **`secondary` takes over what `light` did.** It is a light neutral now
  (`gray-100` in light mode, `gray-600` in dark) rather than the mid gray-blue
  `#6b7785`, so `.btn-secondary` and `.bg-secondary` change appearance.
- **Every `-light` and `-dark` class is gone.** Rename `-dark` to `-inverse` and
  `-light` to `-secondary` across `alert`, `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-bg` and `theme`, plus the `-subtle` and `-emphasis` families. The
  colour-scheme opt-ins keep their names: `.navbar-dark`, `.dropdown-menu-dark`,
  `.carousel-dark` and `.sidebar-dark` are unaffected, as is `.fw-light`.
- **A surface is not a theme colour.** Where `.bg-light` was a neutral panel,
  reach for `.bg-4` or `.bg-3`; where `.bg-dark` was a
  fixed dark backdrop, set `data-coreui-theme="dark"` on the container and let
  the surface classes follow.
- **`--cui-theme-base` is 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 still `base`, and `--cui-{color}` is unchanged.
- **Colour utilities read the slot that stays legible.** `.text-*` points at
  `--cui-{color}-fg` instead 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 the `border` slot, so the outline is a subtle line
  rather than a full-strength one. `.bg-*` and `.border-*` keep reading the
  colour itself.
- **`$light` and `$dark` are removed; `inverse` is new.** Neither `secondary`
  nor `inverse` is a hue of its own: they read `--cui-gray-200` and
  `--cui-gray-900`, and there is no Sass colour behind them to pass to
  `color-contrast()` or the tint/shade functions — read `$gray-*` for that.
- **`$utilities-colors` is removed.** It was a flat `name → colour` copy of the
  map's `base` slot, and no utility reads the raw colour for every family any
  more. Call `map-get-nested($theme-colors, "base")` where you need one, or
  `theme-color-refs("fg")` for the slot a text utility uses.
- **`$theme-lightness-overrides` is now `$theme-state-colors`** and 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

The 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-icon` no 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-color` instead. Set it
  only when the open state is a genuinely different shape — a minus against a
  plus. New tokens: `--cui-accordion-header-icon-color` and
  `--cui-accordion-header-active-icon-color`.
- **A custom icon is now a shape, not a coloured image.** Whatever colour you put
  inside `$accordion-header-icon` or `$carousel-control-prev-icon-bg` /
  `$carousel-control-next-icon-bg` is 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-color` and `$accordion-icon-active-color` are
  `currentcolor` now**, 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-filter` is removed**, with its
  `--cui-carousel-control-icon-filter` custom 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-color` is
  new; `.carousel-dark` pins the dark side of each pair through
  `$carousel-dark-control-color`, `$carousel-dark-indicator-active-bg` and
  `$carousel-dark-caption-color` (previously `$carousel-*-dark` in
  `_variables-dark.scss`). RTL still swaps the two arrows, now through
  `mask-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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`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; `disabled`
  repeats `base`. `button-outline-variant()` and `button-ghost-variant()` are
  **removed** (they are entries in the map now), and `scss/mixins/_buttons.scss`
  no longer exists — the mixin lives next to the map in
  `scss/buttons/_button.scss`.
- **Hover and active shades are computed in CSS** with
  `oklch(from var(--cui-primary) calc(l * …) c h)` instead of precompiled
  `shade-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-amount` and the four
  `$btn-active-*` counterparts. They fed the Sass mixes that no longer happen.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **The interaction shade belongs to the theme, not to the button.** Every
  `.theme-*` class emits `--cui-theme-hover` and `--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-lightness` and `$btn-lightness-overrides` are now
  `$theme-hover-lightness`, `$theme-active-lightness` and
  `$theme-state-colors` in `scss/_theme.scss`.
- **The interaction shade is a runtime knob, and it keeps its saturation.**
  The multipliers are `:root` tokens — `--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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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`/`-lg` pairs. They pointed at `--cui-btn-input-*`, so a value assigned to
  them was overwritten by the token anyway. Configure the frame once on `:root`
  with the `$input-btn-*` variables, or set `--cui-btn-*` on the button.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`$btn-font-weight` is removed, and buttons take their weight from the
  shared frame.** `--cui-btn-font-weight` reads `--cui-btn-input-font-weight`,
  the weight fields and input group addons use, which defaults to
  `--cui-body-font-weight` on `:root`. A page that sets a lighter body weight
  gets lighter buttons with it; `.btn-link` keeps the normal weight. Set
  `--cui-btn-font-weight` on `.btn` to change buttons alone, or
  `--cui-btn-input-font-weight` on `:root` to change buttons and fields together.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`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 `:root` and its name to the map.
- **New tokens on `.btn`:** `--cui-btn-gap`, `--cui-btn-min-height`,
  `--cui-btn-white-space` and `--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.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.btn-close` draws its focus ring as an `outline`,** like every other
  control; `$btn-close-focus-shadow` and `--cui-btn-close-focus-shadow` are
  removed. A box-shadow ring was clipped by the `overflow: hidden` of 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-check` moves onto the `<label>`, with the input nested
  inside.** A toggle button was two elements held together by an `id`/`for`
  pair; it is one element now, so nothing can fall out of sync:

  ```diff
  - <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-group` stops having to reason about a hidden child that is not a
  button. **The old spelling keeps working** — the label carries `.btn` in 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`

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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.

```diff
  "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-colors`
  and `$utilities-links-underline` now carry plain token references**
  (`var(--cui-primary)`, `var(--cui-primary-fg)`) instead of a `color-mix()`
  expression per entry. The
  expression is written once, in the utility's `property` map. If you built a
  map of your own by calling `translucent-css-var()`, call
  `color-var-ref()` instead — same arguments, without the wrapper;
  `translucent-css-var()` is removed, along with the unused `map-loop()` and
  `color-contrast-variables()`.
- **`.text-body-secondary` now 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-border` and `--cui-underline` as 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-selector`** makes 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](https://coreui.io/bootstrap/docs/utilities/api/#child-selector),
[spacing](https://coreui.io/bootstrap/docs/utilities/space/) and
[borders](https://coreui.io/bootstrap/docs/utilities/divide/).

#### Utilities API: theme variants and the `rtl` key removed

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

- **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
  through `light-dark()`, so `.bg-body`, `.text-body-secondary` and friends
  follow the colour mode on their own. The `dark-mode` key of a `$utilities`
  entry is no longer read.
- **The `rtl` key of a `$utilities` entry is no longer read.** It did two
  jobs: `rtl: false` wrapped the output in RTLCSS `/* rtl:remove */`
  directives, and `rtl: true` accepted `("ltr": …, "rtl": …)` value maps that
  emitted a `[dir="rtl"]` counterpart. Direction is expressed with logical
  properties now (the position utilities are `inset-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-x` is written
  as a plain `:dir(rtl)` rule instead of a generator option — `transform` is
  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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

- **The RFS engine is gone** — no `vendor/rfs`, no `font-size()` / `rfs()`
  mixins, no `$enable-rfs`, no `rfs` option 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 literal `clamp()` values in the `$font-sizes` map,
  `$display-font-sizes` and `$legend-font-size`; rendering is unchanged at
  every viewport width.
- **Headings no longer derive from `$font-size-base`** — a `clamp()` cannot be
  multiplied. Override the stop in `$font-sizes` directly.
- A temporary `scss/vendor/_rfs.scss` shim (pass-through `font-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**: `:root` now 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`.
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  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-x` and 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-color` in 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-y` and the rest now default to the shared knob
  (`var(--cui-btn-input-bg)` …). Untouched, the control follows the shared
  surface; overridden through `with()`, 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-padding` and `$form-select-feedback-icon-padding-end`
  are now `calc()` expressions over `var(--cui-control-padding-x)` instead of
  Sass products, and `$form-select-bg-position` carries the `var()` inside it
  (the mirrored keyword comes from the `:dir(rtl)` rule in the same sheet).
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  Sass arithmetic on these variables in your own stylesheets no longer works —
  they are CSS calc strings now, not numbers. Override them with your own
  `calc()` or a plain length instead.
- **Form tooltips reuse the tooltip's runtime tokens.** Add the `.tooltip`
  class 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-tooltip` markup
  keeps rendering exactly as before through built-in fallbacks.
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  The Sass-level derivation is gone: `$form-feedback-tooltip-*` no longer
  default to `$tooltip-*`, so overriding `$tooltip-padding-y` alone 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 to `scss/_tooltip.scss`, the feedback set to
  `scss/mixins/_form-validation.scss`.
- **Validation composes through the tokens**: `.is-invalid` / `.is-valid` set
  `--cui-control-border-color`, `-focus-border-color` and `-focus-ring`
  instead of restating declarations, so state styling follows any control
  automatically.

#### The spacing scale is computed from `--cui-spacer` in the browser

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-spacer-1` … `-12` are `calc()` multiples of `--cui-spacer`, and the
  spacing utilities read the stops.** `$spacers` used to multiply `$spacer`
  when 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 `:root` declares `--cui-spacer`
  (`$spacer`, 1rem) and the stops as `calc(var(--cui-spacer) * n)`, the
  utilities write `var(--cui-spacer-n)`, the negative margins and the
  `.w-*` sizes `calc(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 in `scss/layout/_tokens.scss`, so the grid,
  utilities and reboot bundles each declare it; a `$root-tokens` override 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

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`--cui-border-width-1` … `-5` and `-keyline` are `calc()` multiples of
  `--cui-border-width`, and the `.border-*` width classes read them.**
  `$border-widths` shipped literal pixel values and every `.border-2`,
  `.border-top-3`, `.border-start-5` class carried the length, so changing
  `--cui-border-width` re-weighted the components and left the utilities
  where they were. Now `:root` declares the stops as multiples of
  `--cui-border-width` (`1px`): `-keyline` is half of it, `-1` the base,
  `-2` … `-5` its multiples, and the width classes write
  `var(--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-keyline` keeps 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

The 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`](https://developer.mozilla.org/en-US/docs/Web/CSS/@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 `:root` token, and components
  read it without a fallback. A custom utility that writes a `:root` token name
  must opt out the same way, with `at-property: false` in its `$utilities` entry.
- Compiled output is otherwise byte-identical — the five `@property` blocks are
  the whole diff.

#### The CSS Grid ships, and its responsive classes are container queries

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`$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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`.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:

```html
<div class="grid" style="--cui-columns: 3;">
  <div>
    <div class="grid" style="--cui-columns: 3;">...</div>
  </div>
</div>
```

#### The `cq-*` grid is gone

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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`](https://coreui.io/bootstrap/docs/utilities/container-queries/) and write the query with the
`container-breakpoint-*` mixins.

#### Breakpoint mixins emit range queries

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`media-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`:

```scss
@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](https://coreui.io/bootstrap/docs/utilities/container-queries/).

#### Sass file layout follows Bootstrap v6

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Upstream'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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

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:

```scss
// 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:

```scss
@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](https://sass-lang.com/documentation/breaking-changes/import/)
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](https://coreui.io/bootstrap/docs/utilities/api/#using-the-api) 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

`_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/maps` is gone** and `scss/variables` is not part of the public surface
  any more. Load `.../scss/config`, `.../scss/colors`, `.../scss/theme` or
  `.../scss/utilities` directly, 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**:

```scss
$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-sizes` changed shape** — it was `(1: $h1-font-size, … 6: $h6-font-size)`
  and is now keyed `xs`, `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 to `3rem` and `3.5rem`) continuing the same
  fluid progression, numerically level with the former `.display-5` and `.display-4`.
  <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  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.
- **`:root` gets `--cui-font-size-*` and `--cui-line-height-*` per
  stop**, and `h1`–`h6` read 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`/`-6xl`
  and `$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 the `defaults()` shape — set one key and the rest stays.
  `$font-weight-*` remain and feed `$font-weights` the same way.
- **New: `.fs-{stop}`** alongside `.fs-1` … `.fs-6`, which keep working and render
  identically — `.fs-1` is `.fs-4xl`, down to `.fs-6`, which is `.fs-md`.
- **New: `.text-{stop}`**, which sets `font-size` and `line-height` together.

- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.display-1` … `.display-6` and `.lead` are removed**, with
  `$display-font-sizes`, `$display-font-family`, `$display-font-style`,
  `$display-font-weight`, `$display-line-height`, `$lead-font-size` and
  `$lead-font-weight`. They were a fixed size and weight next to a scale that
  already carries those sizes: `.display-4` is `.fs-6xl`, `.display-5` is
  `.fs-5xl`, and `.lead` is `.fs-lg`; add `.fw-light` for the weight the old
  classes carried and `.lh-1` for the display line height. The three larger
  display sizes have no stop of their own — set `font-size` yourself 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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Focus 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-width` for the
  whole page, or set either on the component itself for one instance.

  ```diff
  - --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-blur` is gone**, along with `--cui-focus-ring-x`,
  `-y` and `-blur`. An outline has no blur or offset vector. **New:**
  `$focus-ring-offset` / `--cui-focus-ring-offset` (defaults to `0`), which
  pushes the ring away from the element, and `--cui-focus-ring`, the
  composite shorthand the mixin reads.
- **`focus-ring()` changed signature** to
  `focus-ring($offset: false, $color: null)`. It no longer emits `outline: 0`.
  Every ring in the library is drawn through it; with no arguments it draws the
  themed ring, and `$color` swaps the hue for one rule — which is how the
  validation states ring in red and green.
- **`--cui-focus-ring-offset` actually applies.** The mixin emits
  `outline-offset` wherever a ring is drawn, so one declaration moves every ring
  in or out. It was previously read by the `.focus-ring` helper alone, which
  made the token look supported while doing nothing on any component.
- `$pagination-focus-outline` is 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-color` or `--cui-focus-ring-width` at 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.

  ```diff
  - --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-color` no 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-width` is
  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

<span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>

Some 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-separator`
  icon 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`](https://coreui.io/bootstrap/docs/forms/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-password` wrapper | `.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-input` | plain `.form-control` — the group strips its frame, so the editable needs no class of its own |
  | `.autocomplete-buttons`, `.form-multi-select-buttons` | gone: 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/-lg` on the group (the components apply it from `size` themselves) |

- **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-tags`
  is `.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-input` and `.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-y` |

  The filled state is `.multi-select-filled`, the Sass partial is
  `scss/forms/_multi-select.scss` with `$multi-select-tokens`, and the component
  stylesheet is `css/components/forms/multi-select.css`.
- **Multi Select starts from `data-coreui-multi-select` only.** 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:

  ```diff
  - <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-y` and 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-action` button) 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 `.popup` primitive.**
  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 `-zindex` variables are replaced by
  `$popup-zindex/-bg/-border-width/-border-color/-border-radius/-box-shadow`
  and 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-style` supplies the state to animate from
  and a discrete `display` transition 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-valid` on the group behave exactly as on a `.form-control`, and the
  state also propagates from the control inside it. The per-family
  `!important` overrides that forced the invalid border in v5 are gone.
- **Markers are hidden with `list-style-type: ""`, not `list-style: none`.**
  Safari drops the `list` role — and VoiceOver stops announcing the item count —
  when a list is styled with `list-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 the `list-unstyled` mixin; your own `list-style: none`
  overrides still win, they just reintroduce the problem.
- **Transitions are emitted inside `prefers-reduced-motion: no-preference`.**
  The `transition()` mixin used to emit a transition and then switch it off
  again under `prefers-reduced-motion: reduce`, so every animated rule shipped
  an override block. It now emits the transition inside a `no-preference` query
  instead, which drops those blocks. The trade-off: a browser that does not
  support the media feature gets no transition at all. Rules that force
  `transition: none` stay 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 `.mw` sitting next to `.min-w` read
  as two conventions for one property. Both ends are now written the same way.
- **`.link-offset-*` is `.underline-offset-*`.** Same three steps, same
  `-hover` variants — `.link-offset-2-hover` becomes
  `.underline-offset-2-hover`. The class sets `text-underline-offset` on 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 `-0` forms) set the
  `border-inline` / `border-block` shorthands, 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-0` is what releases the `min-width: auto` on a flex child so a
  `.text-truncate` sibling can actually truncate; `.animation-{shake|pop}` ship
  their keyframes with them, prefixed so they cannot be redefined by a bare
  `shake` or `pop` of your own, and stand down under
  `prefers-reduced-motion: reduce`; `.color-scheme-{light|dark}` sets CSS
  `color-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-danger` becomes
  `.underline-danger`, and the opacity steps become `.underline-{n}`
  (`.link-underline-opacity-50-hover` → `.underline-50-hover`). Like
  `.underline-offset-*`, the class sets a `text-decoration-*` property on any
  element rather than only on a link, and it now sits beside
  `.underline-thickness-*`, which already carried that name.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **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-4`
  ladder 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 from
  `10` to `100` in steps of ten and mix the colour the companion class wrote into
  `--cui-fg` / `--cui-bg` / `--cui-border` / `--cui-link-color` /
  `--cui-underline` with `transparent` through `color-mix()`. `.lh-md` is the
  body line height (`.lh-sm` and `.lh-lg` read 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-25` and `-75`. The deprecated opacity steps keep
  their old values (`25`, `75`); the new scale rounds them to `30` and `70`.

  What does change: the `--cui-text`, `--cui-text-opacity`, `--cui-bg-opacity`,
  `--cui-border-opacity`, `--cui-underline-opacity` and `--cui-link-opacity`
  tokens are gone (`--cui-text` is `--cui-fg`), so an inline
  `style="--cui-bg-opacity: .5"` no longer fades a `.bg-*` — use `.bg-50`, or
  set `--cui-bg` to a translucent colour. Reboot's `a` reads
  `var(--cui-theme-fg, var(--cui-link-color))` directly instead of mixing the
  link colour through `--cui-link-opacity` on every link.
- <span class="badge fg-emphasis-danger bg-subtle-danger">Breaking</span>
  **`.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-4` | `1rem` | `.375rem` | `.rounded-9` |
  | `.rounded-5` | `2rem` | `.5rem` | none (`.rounded-9` is `1rem`) |
  | `.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-circle` and `.rounded-pill` are unchanged. Each class
  writes `var(--cui-radius-{n})` rather than the stop's value, so a stop
  overridden on `:root` re-rounds the utilities along with the components. The side
  classes set logical corners (`border-start-start-radius`, …), so
  `.rounded-top` flips with the writing direction like `.rounded-start`
  already did.
- **Buttons and controls carry a line height per size.** `:root` emits
  `--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's `min-height` is computed from its own line height. `sm` and
  `lg` keep the body value; the new `.btn-xs` is the only tighter size.
  `--cui-border-width-keyline` (`.5px`) is
  a root token now and `.border-keyline` reads it.
- **New `.rounded-size-*`.** It writes the radius into `--cui-rounded-size`
  without setting `border-radius` itself, 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-top` keeps 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-*-subtle` and `.text-*-emphasis` each
  wrote the property directly, so `.bg-opacity-50` on a `.bg-primary-subtle`
  did nothing while the same class on `.bg-primary` worked. They now declare
  `--cui-bg` / `--cui-border` / `--cui-text` and resolve through the same
  `color-mix()` as the base families. Nothing changes at full opacity.
