Migration to v6

Track and review changes to the CoreUI for React.js components to help you migrate from v5 to v6.

CoreUI for React is the React.js version of the CoreUI components library, so before reading this migration guide, please also check the CoreUI 5 to 6 migration guide — it covers the markup, class names, Sass architecture and design tokens this release is built on. This page covers what changes in the React API.

Dependencies

  • Breaking propTypes are gone from every component, and prop-types is no longer a dependency. Prop validation is the TypeScript types, which every component has carried for years — the runtime copy only duplicated them, and React 19 ignores it anyway. If you were reading SomeComponent.propTypes to introspect a component, read the exported SomeComponentProps type instead.
  • Breaking React 18 is the minimum. peerDependencies moved from >=17 to >=18. The library already used useId, which React 17 does not have, so the old range promised support it could not deliver. React 19 is supported.
  • Breaking Popper is gone; positioning runs on Floating UI. @popperjs/core is replaced by @floating-ui/dom. Update your lockfile — nothing else is required for the default configuration.
  • Breaking popperConfig is now floatingConfig on CDropdown, CPopover and CTooltip, and it takes a Floating UI ComputePositionConfig instead of a Popper one. The popper boolean prop stays and still switches static positioning.
  • Breaking Positioned elements are stamped with data-coreui-placement, not data-popper-placement. Custom CSS, tests or scripts keyed on the old attribute must be updated. The v6 stylesheet draws tooltip, popover and menu arrows from this attribute, so it is not optional.
  • Breaking The exported hooks are renamed: usePopper → useFloating, useDropdownWithPopper → useDropdownWithFloating.

New components

  • CCombobox — a select-like control: a .form-control toggle next to a popup List box, with single and multiple selection, a search field with optional accent-insensitive matching, sections, a checkbox indicator and a hidden select that posts the value and takes part in form validation. Ships with the useCombobox() hook. See the Combobox docs.

  • CDateInput, CTimeInput — section fields: a masked, keyboard-first input where every part of the value (day, month, year, hour…) is its own editable section. <CDateInput type="datetime"> carries the time too. The pickers are built on them, and they work standalone.

  • CDialog and CDrawer — the native-<dialog> popup and side panel with the Bootstrap 6 look and the dialog-* / drawer-* classes. CDialog is a centered floating panel over a blurred backdrop with optional slide-in variants; CDrawer is a floating, inset, rounded panel with sheet, translucent, fit-content and fullscreen variants plus responsive breakpoints. CDialog’s fullscreen takes a breakpoint as well as true, like CModal’s.

    They share their behavior with CModal and COffcanvas, which keep the classic look and the v5 vocabulary — and one callback vocabulary of their own: onShow, onClose and onClosePrevented on both, where the legacy pair says onHide. CModal and COffcanvas are deprecated and removed in v7 — prefer CDialog and CDrawer for new work. Pick one per surface; they are independent components.

  • CListBox — 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. Ships with CListBoxOption, CListBoxSection, CListBoxHeader, CListBoxSelectAll, CListBoxTitle, CListBoxSubtitle, CListBoxEmpty and the useListBox() hook. See the List Box docs.

  • CMenu — contextual overlays with submenus, container teleport, responsive placement and logical (start/end) placements. Ships with CMenuToggle, CMenuContent, CMenuItem, CMenuHeader, CMenuDivider, CMenuText, CSubmenu and the useMenu() hook. See the Menu docs.

  • CNumberInput — a number field with a stepper you can hit, in place of the browser’s own spinner (tiny in some engines, absent on iOS). min, max and step stay native.

  • CTransfer — two lists and four buttons that move values between them, with a select-all header and counter, an optional per-list search, a one-way mode and an announcement of every move through announce(). Composes two CListBoxes and ships with the useTransfer() hook. See the Transfer docs.

Field components

CAutocomplete, CMultiSelect, CDatePicker, CDateRangePicker and CTimePicker are built on one frame and one panel. These two changes apply to all five; the per-component sections below cover what is specific to each.

  • Breaking A field component is the frame. It carries .form-control-group on its own root instead of nesting one, so a field is a direct child of an .input-group like any other control and there is no wrapper level to reach through. Custom CSS written as .date-picker .form-control-group no longer matches anything. On the pickers, the label and the validation feedback also move out of the frame.

    The frame replaces the per-component shells: .autocomplete-input-group, .form-multi-select-input-group and their -sm/-lg variants are gone, the adornments are .form-control-cleaner / .form-control-action (replacing .autocomplete-cleaner, .form-multi-select-cleaner, .autocomplete-indicator, .form-multi-select-indicator), and the size prop puts .form-control-sm / .form-control-lg on the frame itself.

  • Breaking The options/picker panel exists only while it is open. It is built when the field opens and removed once the exit transition has played, so a closed field carries no panel markup at all — code that queried .combobox-popup or .date-picker-popup up front, or held a ref to it across a close, needs to look it up per open.

Theme colors

Breaking light and dark are replaced by inverse. They were theme colors named after a color scheme and neither followed one: <CButton color="dark"> stayed near-black on a near-black page. There are seven colors now — primary, secondary, success, info, warning, danger, inverse — and the Colors type lists them, so color="dark" and color="light" no longer type-check against the union.

  • color="dark" becomes theme="inverse". inverse resolves to gray-900 in light mode and gray-100 in dark, so it stands against the page in both.

  • color="light" becomes theme="secondary". secondary is the light neutral now (gray-100 / gray-600) rather than the mid gray-blue #6b7785, so components already using it change appearance.

  • Where the class was painting a plain panel rather than naming a color, reach for bg-4 or bg-3; where it was a fixed dark backdrop, set data-coreui-theme="dark" on the container.

  • Breaking color on buttons is deprecated in favour of theme. A v6 button is a shape plus a colour: variant picks the shape (solid, outline, subtle, ghost) and theme picks the colour. color renders the v5 spelling btn-{color}, which fuses the two and cannot inherit a colour from an ancestor; it still works and is removed in v7. <CButton color="primary" /> becomes <CButton theme="primary" />, and <CButton color="primary" variant="outline" /> becomes <CButton theme="primary" variant="outline" />. color="link" becomes the link property — a link button is a shape, not a colour, and it takes its colour from the link colour rather than from a theme. color="transparent" is gone with the .btn-transparent class it rendered — use variant="ghost", which shows nothing but the button’s content until hover and carries the base styling on its own.

  • Breaking The theme colour tokens finish their rename. --cui-{color}-text-emphasis is --cui-{color}-fg-emphasis and --cui-{color}-border-subtle is --cui-{color}-border, matching the slot names the components already read. Custom CSS naming the old tokens has to follow; the class names (.text-primary-emphasis, .border-primary-subtle) are unchanged.

  • Breaking Theme colours have no bare token. --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 -base: --cui-primary-base is what --cui-primary was, and -bg points at the same stop. A style prop or stylesheet of yours renames one to one, and setting the palette stop retints the colour:

    - <CBadge style={{ '--cui-badge-bg': 'var(--cui-primary)' }}>
    + <CBadge style={{ '--cui-badge-bg': 'var(--cui-primary-base)' }}>
    
    - :root { --cui-primary: #6f42c1; }
    + :root { --cui-primary-500: #6f42c1; }
  • Breaking The palette is the theme colours. $colors holds primary, success, info, warning, danger and gray; the ten hues ($blue … $cyan) and every --cui-{hue} and --cui-{hue}-{stop} token are gone, as are $secondary and $inverse. Each $theme-colors entry points base and bg at its palette’s 500 stop and the other slots at that scale; secondary reads --cui-gray-200 and inverse --cui-gray-900. No palette token is bare: the literal sits in -500, so --cui-gray-500 is what --cui-gray was, and --cui-primary-500 is the runtime knob for the whole family:

    - :root { --cui-primary-base: #6f42c1; }
    + :root { --cui-primary-500: #6f42c1; }
    
    - <CBadge style={{ '--cui-badge-bg': 'var(--cui-teal-600)' }}>
    + <CBadge style={{ '--cui-badge-bg': 'var(--cui-success-600)' }}>
  • Breaking Under a theme the theme wins over a component color token. A component reads the --cui-theme-* slot on the property and its own token only as the fallback. In v5 the color came from the component’s tokens, so a token you set on a colored alert applied; in v6 a token set on a themed component, or for the whole page in your CSS or a $…-tokens map, changes only the components without a theme. To recolor one themed component, set the slot instead: <CAlert theme="danger" style={{ '--cui-theme-bg-subtle': '#fde2e4' }}>.

  • Variants keep the component token as the override point. Button variants, variant="solid" on an alert, subtle and outline on a badge, subtle on an avatar and outline on a chip map the component tokens to their own slots and paint from the tokens, so there a component token you set wins over the theme. Validation states win over a theme too.

Surface ladder

Breaking The neutral surfaces are a numbered ladder. .bg-body-secondary, .bg-body-tertiary, .text-body-secondary and .text-body-tertiary are deprecated in favor of ordinal names: .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) between the body and the old surfaces. The deprecated names render identically and are removed in v7. The tokens moved with them — --cui-bg-body, --cui-bg-1 … and --cui-fg-body, --cui-fg-1 … are canonical, and --cui-secondary-bg, --cui-tertiary-bg, --cui-secondary-color, --cui-tertiary-color, --cui-body-bg and --cui-body-color remain as aliases until v7. No component prop is affected.

The deprecated surface names sit two rungs down with the rest of the library: .bg-body-secondary renders --cui-bg-3 and .bg-body-tertiary --cui-bg-2, while .text-body-secondary stays --cui-fg-2 and .text-body-tertiary --cui-fg-3.

Sass

  • Breaking A denser spacing scale. $spacers has thirteen steps, 0–12: .25 / .375 / .5 / .75 / 1 / 1.25 / 1.5 / 1.75 / 2 / 2.25 / 2.5 / 3 rem from 1 (v5: .25 / .5 / 1 / 1.5 / 3). Every spacing, gap, space and gutter utility changes value under the same number, and so do the numeric gutter, gutterX and gutterY values of CRow; the v5 steps map to v6 as 1→1, 2→3, 3→5, 4→7, 5→12. p-3 becomes p-5, mb-4 becomes mb-7, <CRow xs={{ gutter: 3 }}> becomes a gutter of 5. Components keep their size, cards aside (see CCard): their tokens read the stops that carry the v5 values.

Breaking The neutral ladder is three maps. $theme-bgs, $theme-fgs and $theme-borders in scss/_theme.scss replace the $fg-*, $bg-*, $border-subtle, $border-muted and $border-emphasized scalars. :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:

- $bg-body: #000;
- $fg-body: #111;
+ $theme-bgs: ("body": #000);
+ $theme-fgs: ("body": #111);

Breaking $enable-important-utilities and $enable-negative-margins are gone. Utilities carry no !important and there is no switch to bring it back — they are emitted in the last cascade layer, so they already beat every component; if your own CSS sits outside a layer and has to lose to utilities, move it into @layer custom. Negative margins are no longer a switch over the whole scale: the $negative-spacers map ships two inline steps, so .me--1, .me--2, .ms--1 and .ms--2 are always available.

Breaking $grid-gutter-width is $grid-gutter-x, and $grid-gutter-y joins it. The row gutter still defaults to 0, so <CRow> and <CCol> render as before unless you set it; $grid-gutter-width stays as a deprecated alias of $grid-gutter-x until v7 — set the new name, the alias only reads.

Breaking $font-sizes sets a line height per stop instead of one value for all headings. xs is 1.25, sm … lg the body leading, 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. The sizes are unchanged, so headings keep their size and change their leading: <CCardTitle> and every h1–h6 follow their stop through --cui-line-height-*.

Breaking Every shadow is a stack of two to four layers with a negative spread instead of one wide blur, so a raised surface reads as lifted rather than smeared. Components that draw a shadow — <CDropdown>, <CModal>, <CPopover>, <CToast> — pick this up with no change of yours. .shadow-xs and --cui-box-shadow-xs are new, and a custom $shadows entry is now a comma-separated list of layers.

New: .w-1 … .w-12, one $spacer per step from the $sizes map, next to the percentage width utilities.

New keys in $zindex-levels: menu, drawer, dialog, on the same values as dropdown, offcanvas and modal, so <CMenu>, <CDrawer> and <CDialog> read their own --cui-z-* token and .z-menu, .z-drawer and .z-dialog join the utilities. The legacy keys stay; both spellings of a pair resolve to one number.

Breakpoints

Breaking 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); xs, sm and md are unchanged. <CContainer> grows with them: 1200px at xl (was 1140) and 1440px at 2xl (was 1320).

Every prop and prop value that named the top tier is renamed. 2xl is not a valid JSX attribute name, so the four grid-shaped props are passed with a spread:

- <CCol xxl={6} />
+ <CCol {...{ '2xl': 6 }} />

- <CRow xxl={{ cols: 3 }} />
+ <CRow {...{ '2xl': { cols: 3 } }} />

- <CContainer xxl />
+ <CContainer {...{ '2xl': true }} />

- <CPlaceholder xxl={4} />
+ <CPlaceholder {...{ '2xl': 4 }} />

The props that take a breakpoint as a value change without ceremony:

ComponentPropBeforeAfter
CCol, CRow, CContainer, CPlaceholderthe tier itselfxxl'2xl'
CDialog, CModalfullscreen"xxl""2xl"
CDrawer, COffcanvasresponsive"xxl""2xl"
CDropdownalignment{ xxl: 'end' }{ '2xl': 'end' }
CHeader, CNavbarcontainer"xxl""2xl"
CNavbarexpand"xxl""2xl"
CListGrouplayout"horizontal-xxl""horizontal-2xl"
CTableresponsive"xxl""2xl"
CMenu, CPopover, CTooltipplacement"xxl:top""2xl:top"

CIcon’s size is not a breakpoint and keeps every value it had — size="xxl" is unchanged.

Components

The visible family

  • visible is deprecated in favour of defaultShow / show / onShowChange on CAlert, CDialog, CDrawer, CDropdown, CMenu, CModal, COffcanvas, CPopover, CSidebar, CToast and CTooltip. The old prop was semi-controlled: it fed the internal state, but the component still closed itself, so the prop and reality desynchronized on the first Escape or backdrop click — a confirm-before-close dialog or a pinned tooltip could not be built on it. The new props say which mode is which:

    • defaultShow — the uncontrolled starting point, read once on mount.
    • show — controlled. The component renders exactly what you pass; close and trigger gestures (Escape, a backdrop or outside click, the dismiss button, hover, the autohide timer) only call onShowChange, so it has to be fed back.
    • onShowChange — fired with the requested state in both modes.

    visible keeps its old behaviour unchanged, is ignored when defaultShow or show is set, and is removed in v7.

  • show replaces visible on the purely controlled components too — CCollapse, CTabPane, CTabPanel and CBackdrop take show, and visible stays as a deprecated alias that loses to it. These never owned their state, so nothing changes in behaviour; the library speaks one word for the same thing.

  • CSubmenu takes defaultShow / show / onShowChange in place of its visible, which was a seed the submenu silently overwrote on hover; a submenu can be controlled now.

  • Breaking CNavGroup moves to defaultShow / show / onShowChange, and show is purely controlled — a group given show renders what the prop says and the toggler only calls onShowChange, so a bare show without the callback freezes the group. An expand-by-default visible maps to defaultShow. visible, defaultVisible and onVisibleChange stay as deprecated aliases; the function form of toggler receives { show } (with visible still present).

  • CToaster renders the toasts of a store as controlled — each one takes show from its open flag, and the close button and the autohide timer go through toaster.close(id).

Controlled value props

Breaking The value and state props are fully controlled when set — the same contract the visible family moved to, applied to the props that hold a component’s value. They used to be semi-controlled: the prop fed the internal state, but the component still wrote to it, so the prop and reality desynchronized on the first user interaction. Now the component renders exactly what you pass, an interaction only calls the callback, and nothing moves until you feed the change back — a bare value prop without its callback freezes the control. Code that passed one as an initial value migrates to the new default* twin, which seeds the uncontrolled state once, on mount:

componentcontrolled propuncontrolled seed
CAutocomplete, CMultiSelectvaluedefaultValue
CButton (toggle mode)activedefaultActive
CChipselecteddefaultSelected
CDatePickerdatedefaultDate
CDateRangePickerstartDate, endDatedefaultStartDate, defaultEndDate
CPasswordInputshowPassworddefaultShowPassword (with the new onShowPasswordChange)
CRangeSlidervaluedefaultValue
CSmartTablesorterValue, columnFilterValue, globalFilterValue, activePage, itemsPerPage, selecteddefaultSorterValue, defaultColumnFilterValue, defaultGlobalFilterValue, defaultActivePage, defaultItemsPerPage, defaultSelected
CTimePickertimedefaultTime
  • CAutocomplete, CMultiSelect: the per-option selected flags are a seed, ignored entirely when value or defaultValue is set, and they no longer re-apply on later renders of the same option. In CAutocomplete a flag applies once, when its option first appears. In CMultiSelect it applies only to the options the component starts with: options that arrive later never change the selection, and resetSelectionOnOptionsChange empties it and takes the marked options when the values change. With value set the reset only requests the empty selection through onChange. A native reset of the form with value set requests the first value through onChange; it used to leave the controlled selection to you.
  • Form controls answer a native form reset the same way, controlled ones included. CAutocomplete, CChipInput, CCombobox, CDateInput, CDatePicker, CDateRangeInput, CDateRangePicker, CFormRange, COneTimePassword, CRangeSlider, CRating, CTimeInput and CTimePicker with a controlled value used to keep it on a reset; they now ask for the value they started with through their change callback and show your value until you take it, so a page that resets its own state from the form’s reset gets that request after its handler. CRangeSlider and CFormRange now call onChange when an uncontrolled reset moves the value, and CStepper keeps the validation marks of a form reset a listener cancels.
  • CSmartTable: a controlled prop change no longer echoes its own callback. Passing a new sorterValue used to fire onSorterChange back at you (and likewise for the other five); the callbacks now fire only when the user acts. The page clamp is a request in controlled mode — when the items shrink below the current page, the table calls onActivePageChange(1) instead of moving itself.
  • CLoadingButton: loading is the controlled mode and wins over timeout. While loading is set the button never stops the spinner itself and timeout is ignored; timeout without loading is the uncontrolled auto-stop it always was.
  • CButton: a toggle button’s active is controlled when set — a click only fires onClick and the pressed state follows your prop; seed an uncontrolled toggle with defaultActive. Without either prop nothing changes.
  • CCalendar: equal re-renders no longer reset the view. A parent re-rendering with an equal calendarDate, startDate or endDate (a fresh Date object holding the same value) used to yank back the navigated month and cancel an in-progress range selection; only a real value change syncs now. Month names also follow locale and monthFormat changes at runtime.

Validation state

Deprecated One validationState prop replaces the valid and invalid booleans on every form control, field and picker, and on CFormFeedback. validationState="invalid" renders is-invalid and sets aria-invalid on the control, "valid" renders is-valid, and any custom state from the $form-validation-states Sass map renders is-{state} — two independent flags could neither express a third state nor refuse valid and invalid at once. On the components that had them in v5 the booleans keep working as deprecated aliases (with both set, invalid wins, where they used to stack), and are ignored whenever validationState is set; CCheckbox, CChipInput, CCombobox, CDateInput, CDateRangeInput, CNumberInput, CRadio, CSwitch and CTimeInput take only validationState. Four side effects worth knowing:

  • feedback follows the state: it renders only while validationState is set, styled after it, and describes the control in every state, not only the invalid one.
  • Every control that reads validationState="invalid" now sets aria-invalid="true", except CCombobox, whose toggle is a button and takes only the message in aria-describedby; a control without a validation state still sets none.
  • Breaking validationState="invalid" (or the deprecated invalid) blocks the submit on CFormInput, CFormSelect, CFormTextarea, CNumberInput, CPasswordInput, CCheckbox, CRadio, CSwitch, CFormCheck and CFormSwitch, with the text of the invalid feedback as the browser’s message, the way it already did on the fields that build their own markup. Each control calls onValidationStateChange(undefined) when the user changes its value or the form is reset; pass your state setter there (with the deprecated invalid, set it to false there instead), or a form you gave an invalid state stays blocked until you clear it yourself. See Validation.
  • Breaking CMultiSelect, CChipInput, CCombobox, COneTimePassword, the date and time fields, the pickers and CAutocomplete take and show their validation state the same way, described under Form components. A field a validation reports shows the state on the component’s frame, and <CForm> and CStepper leave the field that carries the value to the component. After taking the result over with preventDefault() in onValidate, keep event.isValid rather than calling checkValidity(): the call reports every invalid field again, and the components then show their error.

Help text and feedback ids

text and the feedback get ids of their own. A form control used to hand its aria-describedby to the text and to the feedback as their id, so the help text and the error shared one id: the control read the help text and never the error, a valid control with feedbackInvalid was described by the hidden error, and an aria-describedby that pointed at an element of the page gave that id to a second element. Now text, feedback, feedbackInvalid and feedbackValid get generated ids, and the control lists them in its own aria-describedby after the one you pass: the text always, feedback while validationState is set, feedbackInvalid only while the control is invalid and feedbackValid only while it is valid, whether that state comes from validationState or, in a data-coreui-validate form, from the browser’s :user-invalid and :user-valid (the date and time fields and the pickers use the field’s own verdict instead). A validationState you set takes precedence. Pass aria-describedby only to point at elements of your own; a value you passed so that the text would carry it is no longer needed, and CSS or scripts that found the text by that id need another hook. The pickers drop the undocumented describedby prop for the same reason, and CFormControlWrapper and CFormControlValidation replace it with descriptionId, the prefix the ids are built from (<descriptionId>-label, -text, -feedback, -feedback-invalid and -feedback-valid).

CAccordion

  • Breaking The accordion is built on the native <details> and <summary> elements. CAccordionItem renders a <details class="accordion-item"> and CAccordionHeader its <summary>, so the browser owns opening, closing, keyboard interaction and the disclosure semantics — none of it depends on the component being mounted.

  • Breaking CAccordionButton is removed, together with .accordion-button, .accordion-collapse and .collapsed. The trigger is the <summary> itself and the panel is the element’s own content, revealed through ::details-content. CAccordionHeader already rendered the button for you, so markup written against the components needs no change; custom CSS keyed on those classes does. Style the open state with .accordion-item[open] > .accordion-header.

  • Breaking CAccordionHeader renders a <summary>, not an <h2>. A <summary> is not a heading — where the accordion divides a page into sections, wrap the label in an <h2>–<h6> inside the header. The heading inherits the summary’s typography, so nothing looks different.

  • Grouping is the name attribute. Without alwaysOpen the items of one accordion share a generated name, and the browser keeps a single one open.

  • Breaking The open items follow the controlled / uncontrolled split the rest of the library uses. activeItemKey fed a useState initial value, so it read like a controlled prop and behaved like a default: changing it after mount did nothing, and nothing reported back when the user opened an item. It is deprecated in favour of three props that say which is which:

    • defaultOpenItemKeys — the uncontrolled initial value, where activeItemKey maps.
    • openItemKeys — controlled. The accordion renders exactly what you pass, so it has to be fed back from onChange.
    • onChange — fired with the new list in both modes.

    They are named for what an accordion holds. active is the vocabulary of a selection — one item out of a set, which is what CTabs has — while a <details> element is open or closed, and that is what CAccordionItem calls its own prop.

    All three take a single key as well as an array; an array only opens more than one item under alwaysOpen, since the HTML Standard forbids leaving more than one open item in a name group.

  • New: onShown / onHidden on CAccordionItem, fired from the element’s own toggle event — so they report the state whichever path opened the item, including the browser closing a sibling of the same name.

  • The panel animation is CSS-driven through interpolate-size: allow-keywords, which only Chromium implements today. Elsewhere the component measures the two sizes and the stylesheet times the move, exactly as CCollapse does; that part goes away, without a breaking change, once Firefox and Safari ship calc-size().

  • The indicator can be an element. Put a node carrying accordion-icon in the header and it replaces the chevron the header draws for itself; a second one marked accordion-icon-active is swapped in while the item is open.

  • New: theme on CAccordion tints the open item. Closed items are untouched, so it marks what is expanded rather than recoloring the control.

  • Breaking CAccordionItem takes open, and visible is deprecated. The item renders a <details> element and that is what the attribute is called there, so the prop no longer carries a second word for the same state. It is also the controlled form for a single item, for the cases openItemKeys does not cover; visible keeps working.

  • A collapsed item stays findable. <details> keeps its content searchable and the browser expands the item on a find-in-page match, so the accordion needs no counterpart to CCollapse’s hiddenUntilFound.

CAlert

  • dismissible is no longer required to position a close button. An alert makes room for a <CCloseButton /> rendered as its direct child and positions it on its own. The prop keeps working and stays the way to get the built-in button. The button has to be a direct child to be pushed to the trailing edge — one nested in a wrapper is laid out by that wrapper.
  • Breaking color applies .theme-{color}, not .alert-{color}. The two emit a byte-identical block of --cui-theme-* declarations — the .alert-* names survive in v6 only as back-compat aliases, and every other themeable component reads the same class. Markup that still carries .alert-success renders exactly as it did; the component just stops generating the older spelling.
  • Breaking variant="solid" renders .alert-solid alongside the theme class, not bg-{color} text-white. The old pair hard-coded white text, which fails WCAG AA on five of the eight theme colors — 1.10 on light, where white on near-white is invisible. .alert-solid reads the theme’s own contrast token, so every color lands above the threshold, including one you define yourself. The value also narrows from 'solid' | string to 'solid'; v6 has no other alert variant.
  • Breaking The alert lays its children out in a row. It is display: flex with --cui-alert-gap between children, so an icon beside a label needs no d-flex and no spacing utility. Text mixed with inline elements 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.
  • An alert rendered with transition fades in when it is added to the page, not only out when it is dismissed. Nothing to change; @starting-style supplies the state the animation was missing in that direction.
  • An <hr> inside an alert takes the alert’s border color rather than the page’s neutral grey: the theme’s border color, or --cui-alert-border-color without a theme, so a separator follows a border you recolored. With variant="solid" the border has the surface color, so the separator takes the theme’s border color instead.
  • Breaking The alert is rounded with --cui-radius-7 (.75rem), like the dropdown, popover, toast, dialog, drawer and accordion; v5 used .375rem. Set --cui-alert-border-radius: .375rem to keep the v5 corners.
  • Breaking --cui-alert-border is gone. The border is written in the rule from --cui-alert-border-width and the border color, so that a theme can color it. Set those two instead, and border-style on the alert for a dashed or dotted line.
  • Breaking .alert-dismissible is no longer rendered. The class carries no styling in v6 — the close button is a flex item pushed to the end with margin-inline-start: auto, so the alert reserves no extra padding and needs no marker. The dismissible prop is unchanged and still renders the button and the self-dismiss behaviour; only the dead class is gone. CSS of your own that targeted it should target .alert or the button instead.

CAutocomplete, CMultiSelect

  • loading shows a row below the options instead of covering them. The white cover with a spinner that sat over the options while loading was set is gone: the list is marked aria-busy="true", a row with a spinner and the new loadingLabel text ('Loading…') shows below the options and is announced to screen readers when the shown list starts loading or the list shows up loading, and the options it shows can still be picked with the pointer and the keyboard.

  • Breaking The list inside the panel is a CListBox, so it carries the list-box-* classes: list-box, list-box-options, list-box-option (+-label, -description, -indicator), list-box-section (+-label), list-box-empty, list-box-header, list-box-select-all, with the active, selected and indeterminate state classes. The panel itself keeps combobox-popup. The combobox-option*, combobox-optgroup*, combobox-options*, combobox-header and combobox-all classes are gone, and so are their --cui-combobox-option-*, -optgroup-* and -all-* properties — theme the list through --cui-list-box-*. The former autocomplete-dropdown / autocomplete-option(s) and form-multi-select-dropdown* / -options* / -option* / -all* / form-multi-selected classes are gone as well. This markup is generated by the components, so only custom CSS targeting the panel internals needs updating.

  • Breaking selectAllStyle and optionsGroupsStyle are removed from CMultiSelect. One marker serves the whole list: indicator decides what the options, the group labels and the select-all button all draw.

  • Breaking role="listbox" is on .list-box-options, not on the panel, and a group is wrapped in .list-box-section rather than being a label followed by loose siblings. In CMultiSelect the panel also holds the search field and the select all button, which the old placement claimed as options.

  • onShow and onHide fire once per transition, and neither fires on mount. CMultiSelect used to fire onHide every time the menu opened.

  • Breaking CAutocomplete keeps the selection and the typed text apart. value and defaultValue take an option value, compared as text, so 1 and '1' select the same option; a string used to be matched against the labels. value={null} selects nothing. onChange reports an option or null, never typed text: text the user types without picking an option stays in the field and is posted under name. The text is a state of its own: defaultInputValue starts it, inputValue controls it, and the new onInputChange reports every change, picks included, while onInput reports the search the options are filtered by. Enter on text that names no option no longer reports it through onChange. From onChange store option.value, option.label for an option without a value, or the string of a plain string option.

  • CAutocomplete shows the cleaner for typed text when allowOnlyDefinedOptions is off, keeps it out of the Tab order, and leaves the focus in the field after a click.

  • Breaking CAutocomplete keeps the focus in its input. The arrows no longer walk the focus into the panel: the input keeps it and reports the highlighted option through aria-activedescendant, which the option marks with the active class. The input also keeps the keys that belong to its text — Home, End and Space move the caret rather than the highlight.

  • Breaking CMultiSelect 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. Tab cycles through the panel and the search box without leaving them, and Escape closes the panel and hands the focus back to the control. The select-all button is a control over the list rather than a value in it, so it is reached with Tab; selectable group labels are in the arrow order in its place.

  • Breaking CMultiSelect’s search no longer takes 'global'. Typing reaches the search box from anywhere in the component by default, which is what that value used to turn on, so search="global" becomes search and search={{ external: true, global: true }} becomes search="external". 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 (with search="external" it does nothing, since the list holds only the last results).

  • Breaking CMultiSelect’s search no longer takes an object. It takes true, false or 'external', so search={{ external: true }} becomes search="external".

  • Breaking CAutocomplete’s search takes only 'external', and global typing is globalSearch, on by default. search="global" goes away: typing on the picker button or in the options panel reaches the field without it. The object form goes too, so search={{ external: true, global: true }} becomes search="external". Set globalSearch={false} to keep a keystroke where the focus is. The space bar and shortcuts with Ctrl or Cmd stay with the button that has the focus, and a character typed with AltGr or Option reaches the field like any other, in CMultiSelect too.

  • CAutocomplete and CMultiSelect read the typed text without the spaces around it, in the filter, the highlight, the hint, Enter and leaving the field, and CMultiSelect creates an option from the trimmed text. A query of spaces lists every option. In CMultiSelect the search matches option labels only: a group whose own label matches no longer keeps all of its options, and a group without options is not listed.

  • CMultiSelect blocks the submit while validationState is 'invalid', and asks to drop the state with onValidationStateChange(undefined) when the user changes the selection or the form is reset. A native form reset now restores the selection given by defaultValue or the options’ selected. See Validation.

  • CAutocomplete blocks the submit while validationState (or the deprecated invalid) is 'invalid', and asks to drop the state with onValidationStateChange(undefined) when the user changes the text or the form is reset. The frame also shows the state a validation reports for the input, where it used to show only the state you gave. See Validation.

  • Keyboard, CMultiSelect: arrow keys skip disabled options.

  • Keyboard, CAutocomplete: ArrowDown out of the input highlights the first enabled option, and Escape hands the focus back to the input. A closed field renders aria-expanded="false" instead of omitting the attribute.

  • Breaking CMultiSelect’s search field no longer reaches the server. The input that filters the options carried a name built from the field’s own, and a plain text input is serialized, so every form holding a CMultiSelect submitted whatever the user had typed to narrow the list. It carries no name now and the search… key is gone from the payload.

  • Breaking An unnamed CMultiSelect stays out of the form. The native select took name={name ?? uniqueId}, so a component the page had not named still submitted its value under React’s generated id, a key like :r1: that nobody chose. The attribute is written only when name is given. CMultiSelectNativeSelect carried a second fallback of ${id}-multi-select for the same purpose; it was unreachable through CMultiSelect and is gone too.

  • Breaking An unnamed CAutocomplete stays out of the form too. Its input took a generated name when the prop was not set, so a form carried a key nobody chose. It does not now. The generated id is unaffected.

  • Breaking CAutocomplete’s typeahead hint no longer takes the field’s id. With showHints, the decorative hint input rendered the same id as the real one, so document.getElementById, a label[for] and the floating label association all resolved to the hint. It carries no id now.

  • Deprecated CMultiSelect: selectionType="tags" is "chips", and ariaTagDeleteLabel is ariaChipRemoveLabel. The selection renders chips, so the mode carries their name and "chips" is the default. "tags" renders the same until v7. The old label prop still names the remove button and takes precedence while it is set.

  • Breaking CMultiSelect’s classes drop the form- prefix, so they carry the component’s name like .autocomplete, .chip-input and .date-picker. There is no alias; update CSS that targets them.

    v5v6
    .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, and the component stylesheet is @coreui/coreui-pro/css/components/forms/multi-select.css.

  • CMultiSelect: Backspace in the empty search box focuses the last chip. It removed the last value at once; the first press now moves the focus to the last chip and the second removes it, as in CChipInput. The arrow keys move between the search box and the chips, the selection is one tab stop, removing a focused chip moves the focus to its neighbour, and a value removed through a chip, Backspace or the cleaner is announced (ariaRemovedAnnouncement). A single select and the counter and text selections still remove the last value at once.

CAvatar

  • Breaking textColor is removed. It wrote a single fg-{value} class, and the values it listed from v5 (body-secondary, body-tertiary, *-emphasis, black-50, white-50) had no rule in v6, so the text kept its inherited color. Put the color utility on the component instead: className="fg-2" for body-secondary, fg-3 for body-tertiary, fg-emphasis-primary for primary-emphasis, text-body-emphasis for body-emphasis, fg-white fg-50 and fg-black fg-50 for white-50 and black-50, and fg-{color} for a theme color.
  • Breaking color applies a theme class, not a background utility. It rendered bg-{color}, which left the letters on the inherited color and is why the docs paired it with a hand-picked textColor — a recipe that failed WCAG AA on five of the six examples they shipped. .theme-* supplies the background and the color that reads on it, so drop the textColor that went with it. A bare <CAvatar> is a neutral circle instead of a transparent one.
  • New: variant="subtle" — a tinted avatar that keeps the theme color in the letters rather than behind them.
  • New: ariaStatusLabel names the status indicator, rendering role="img" with the label. The indicator is color and nothing else, so a state that carries meaning needs a name; aria-label on a bare <span> has no role to attach to and may be ignored. Without the property the indicator stays decorative, as before.
  • size is typed on CAvatar and CAvatarStack: xs, sm, md, lg, xl — the five sizes the stylesheet ships — instead of any string.
  • The Sass variables behind the size moved: --cui-avatar-width / -height become --cui-avatar-size, and --cui-avatar-status-width / -height become --cui-avatar-status-size. Only inline overrides of those names need updating.

CBadge

  • Breaking textColor is removed. It wrote a single fg-{value} class, and the values it listed from v5 (body-secondary, body-tertiary, *-emphasis, black-50, white-50) had no rule in v6, so the text kept its inherited color. Put the color utility on the component instead: className="fg-2" for body-secondary, fg-3 for body-tertiary, fg-emphasis-primary for primary-emphasis, text-body-emphasis for body-emphasis, fg-white fg-50 and fg-black fg-50 for white-50 and black-50, and fg-{color} for a theme color.
  • Breaking color applies a theme class, not a background utility. It rendered bg-{color}, which in v6 leaves the badge on the inherited text color instead of white — the .badge-* namespace carries the variants now, and the color comes from .theme-*. The prop is unchanged at the call site; only the class it emits moved. A bare <CBadge> is neutral rather than invisible, so a badge that relied on a utility for its background can drop it.
  • Breaking color no longer takes a gradient. v6 removes the bg-*-gradient utilities the value mapped to.
  • Breaking textBgColor is gone. It emitted .text-bg-{color}, which resolves to the same two values color now supplies through the theme class — --cui-{color}-contrast on the text and the colour itself behind it — so the two props had become one. Keeping it was also destructive: .text-bg-* ships in the helpers layer and writes color and background-color directly, over the tokens .badge reads, so pairing it with variant="outline" filled in the background the variant had cleared. Use color.
  • New: variant — "subtle" or "outline", alongside the default solid.
  • size also takes "lg".

CBreadcrumb

  • Breaking A step is a padded, hoverable target. CBreadcrumbItem renders .breadcrumb-link on the link its href produces, and wraps an active step in a <span className="breadcrumb-link"> so every step keeps the same height. To keep the plain text link of v5, render your own link as a child instead of passing href.
  • New: CBreadcrumbDivider — a separator you put in the markup, carrying whatever you give it: an inline SVG, an icon, a character. An empty one renders the default divider. The automatic ::before divider steps aside wherever an element is placed, so the two never double up, and the component marks itself aria-hidden.
  • A step can open a menu. Wrap the step’s content in a CMenu whose CMenuToggle takes the new custom prop, and the toggle behaviour is wired onto your own .breadcrumb-link button instead of a CButton. See Collapsed items.

CCalendar

  • 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 a single disabled Monday disabled its week, 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.
  • Breaking onStartDateChange and onEndDateChange receive the Date first, the same shape the pickers use, and the value formatted per selectionType second — a Date for day selection but a string for week ('2026W27'), month ('2026-01'), quarter ('2026Q1') and year ('2026'). v5 passed them the other way round.
  • Breaking CCalendar no longer reports a date that stays empty. Picking a day before the start of a range, or an end across a disabled day, used to call onEndDateChange(null) although no end had been picked, and clearing a range across a disabled day before a start was picked called onStartDateChange(null). A day picked again is still reported, as in the Vue library.
  • Breaking selectAdjacementDays and showAdjacementDays are spelled selectAdjacentDays and showAdjacentDays on CCalendar and every picker built on it. The typo is fixed in the vanilla and Vue libraries in the same release.
  • Breaking onDateHover is gone from CCalendar, CDatePicker and CDateRangePicker, as cellHover is from the vanilla calendar. It served the hover preview in the field, which went with previewDateOnHover. To follow the pointer or the focus over a CCalendar, listen for mouseover or focusin on an element that wraps it: 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. The pickers have no replacement for it.
  • Breaking 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].
  • Breaking navigation and navYearFirst are gone from CCalendar, CDatePicker and CDateRangePicker. With navigation={false} the month and year buttons stayed on screen but did nothing; the arrows are always there now. To hide them, hide .calendar-nav-prev and .calendar-nav-next in your CSS. The month and year buttons still open the months and years views; to keep the calendar on the month you set, hide .calendar-nav and give the month a heading of your own. The order of the year and the month follows the locale instead of navYearFirst: the year comes first in locales that write it first (for example Japanese, Chinese, Korean, Hungarian or Lithuanian).
  • Breaking The calendar’s classes and data attributes are the vanilla calendar’s. A panel is .calendar without the view class (days, months, quarters, years) and carries its position in data-coreui-calendar-index; the view is named on the cell content instead (.calendar-cell-inner.day, .month, .quarter, .year), and quarter and year cells no longer carry quarters / year. The navigation buttons carry btn-double-prev, btn-prev, btn-month, btn-year, btn-next and btn-double-next, and data-coreui-selectable is empty instead of "true". A rule that styled a panel by its view needs .calendar:has(.calendar-cell-inner.month) instead of .calendar.months, and one that styled .calendar-cell.quarters or .calendar-cell.year needs .calendar-cell:has(> .calendar-cell-inner.quarter) or .year, since the cell states (selected, range, disabled) stay on the cell.
  • Breaking Setting calendarDate to null keeps the month on screen. v5 moved the calendar back to today; to show today, pass today’s date. An empty calendarDate (null or '') at mount no longer opens the calendar on today: it opens on startDate or endDate, as when calendarDate is left out; v5 opened on today for null.
  • onViewChanged is deprecated in favour of onViewChange, the tense every other callback uses; both fire while the old name is set.
  • The grid answers the full APG keyboard model. On top of the arrows: Home / End move within the focused week, Page Down / Page Up move a month at a time (a year in the months and quarters views, the twelve years of a page in years), and Shift with either moves a year in the days view. A jump never lands outside minDate / maxDate: focus stops on the limit, also when that date cannot be picked. Every cell carries its own data-coreui-date.
  • selectionType="week" is reachable from the keyboard. The day cells take no focus in that mode because the row is what gets picked, and the row itself was not a tab stop — so the grid had none at all and week selection was mouse-only. The rows are tab stops now, and Home / End move to the first and last week of the month.
  • Picking a date keeps the months on screen. A picked day of an adjacent month (selectAdjacentDays) used to turn the calendar to that month, and so did any pick the parent passed back through startDate: a start date picked in the second month of CDateRangePicker shifted both months by one. A picked day of an adjacent month now shows as selected with an outline on a subtle background instead of the solid fill. The view still follows calendarDate, and without it a start date the parent changes to a new instant, the day already selected included (a Today button after browsing); it stays for the calendar’s own picks passed back before the view moves and for the same instant passed again. An end date moves it while no start date is set, unless the calendar already holds that end.
  • 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.
  • The range preview no longer stays on the date just picked, and in week selection it no longer drops while the pointer crosses a day of the adjacent month inside the row it previews.
  • 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 prop takes a function of the week number to name it yourself.
  • Breaking The years view names its double arrows with new ariaNavNextYearsLabel / ariaNavPrevYearsLabel props ('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 prop ('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 props ('Selected date: {date}', 'Selected range: {start} to {end}'); before, picking a date with Enter left a screen reader silent. A page that translates the other calendar labels translates these two as well.
  • The toggle of CDatePicker and of CDateRangePicker 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.
  • 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.
  • Breaking A cell drawn by renderDayCell no longer gets an aria-label, so its content is what a screen reader reads. Every render prop 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), which also leaves their title tooltip, 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.
  • renderDayCell receives isToday: false for the copy of today among an adjacent month’s days, which takes neither the today class nor aria-current="date" either, and in day selection isInRange: false for a day among an adjacent month’s days that only fills the grid, which the range is not drawn on. With selectAdjacentDays the range and its hover preview are now drawn on the adjacent month’s days as well, and isInRange stays true for them.
  • 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 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 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 or a picker 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.

CCallout

  • New: theme. It sets the color context and renders a .theme-* class, so the callout also picks up a theme class placed on any element above it. The class colors the leading edge and tints the border.
  • Deprecated color still works and is removed in v7. It renders the same .theme-{color} class as theme and loses to theme when both are set, so the modifier classes the component used to emit (.callout-{color}) are gone from its output.
  • A callout without a color no longer draws its leading edge in currentColor. The class read a custom property that only the color modifiers declared, so on a bare <CCallout> the declaration was invalid and the property fell back to its initial value. It now defaults to the callout’s own border color.
  • The 1px border of a colored callout is tinted instead of staying neutral. The leading edge is unchanged.

Deprecated color classes

  • .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, not in the component stylesheets. The components render theme classes now, so none of them emit those spellings — the sheet only serves markup you wrote by hand. Import it while you still need it, and drop it once your markup uses the theme classes. They are removed outright in v7.
  • The color property renders the theme classes. <CButton color="primary" /> renders btn btn-solid theme-primary rather than btn btn-primary, so an app that has not moved off color keeps its styling without loading the legacy sheet.
  • CFormCheck and CFormSwitch import coreui-legacy.css themselves. They render the v5 .form-check markup by design; the sheet comes with them, so nothing changes for you.

CCard

  • Breaking textColor is removed. It wrote a single fg-{value} class, and the values it listed from v5 (body-secondary, body-tertiary, *-emphasis, black-50, white-50) had no rule in v6, so the text kept its inherited color. Put the color utility on the component instead: className="fg-2" for body-secondary, fg-3 for body-tertiary, fg-emphasis-primary for primary-emphasis, text-body-emphasis for body-emphasis, fg-white fg-50 and fg-black fg-50 for white-50 and black-50, and fg-{color} for a theme color.
  • Breaking A card is roomier, rounder 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. CCardHeader, CCardFooter and a CListGroup 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 a translucent card, and the radius is --cui-radius-7 (.75rem; v5 used .375rem). A card filled with textBgColor or color keeps that neutral frame; add the matching border-<color> class to frame it in the fill colour. To keep the v5 look, set --cui-card-spacer-y and -x to var(--cui-spacer-5) and --cui-card-border-radius to .375rem on the card.
  • Breaking CCardLink is removed. .card-link is gone from v6 — it existed to space sibling links with an inline margin, which only worked while they were inline, and .card-body is a flex column with --cui-card-body-gap now. Wrap the links yourself: <div className="hstack gap-4"><CLink …/><CLink …/></div>.
  • Breaking .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-* 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 <CCardBody>, a list group or a bottom image that follows a top image, 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 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. <CCardHeader> and <CCardFooter> draw their own divider, as before.
  • <CCardGroup> 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.
  • Breaking color no longer takes a gradient. v6 removes the bg-*-gradient utilities the value mapped to, so the Gradients union is gone from the prop’s type. Reach for .bg-gradient alongside the color if you want the sheen.
  • style on CProgress lands on the track. It used to travel to the bar with the other attributes; now it stays on the .progress, as it does in the Vue edition, so the segment and gradient variables set there reach the mask and the bar alike. The other HTML attributes still go to the bar.
  • Gradient bars. --cui-progress-bar-bg-image paints the bar with an image over its colour, so a gradient is one variable in style; stripes layer over it and have their own --cui-progress-bar-stripes and --cui-progress-bar-stripe-color.
  • segmented and segments on CProgress. The track becomes a row of rounded segments — --cui-progress-segment-min-width wide and --cui-progress-segment-gap apart, or at most segments of them — and the bar fills whole segments only, recounted as the track resizes. Corners come from --cui-progress-segment-border-radius.
  • Breaking .card-header-pills is gone. It 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 inner edge they touch the border. Drop the class from <CNav variant="pills">.
  • Breaking <CCardGroup> attaches from a container query, not a media query. The sm breakpoint is the width of the nearest query container, not the viewport. Mark an ancestor with .contains-inline; without a container the query has nothing to read and the cards stay stacked at every width. The same applies to <CListGroup> with layout="horizontal-*".
  • New: theme paints the card’s frame and caps with a theme color, with contrast text in the header and footer; the body keeps the card background. It is a different axis from color, which still applies the bg-{color} utility across the whole surface, and from textBgColor. Put the class on a single section — <CCardFooter className="theme-danger"> — and only that section follows it.
  • New: variant="subtle" swaps the solid fill for the theme’s subtle background and border, and new: translucent blurs whatever is behind the card and thins the caps to match. They are independent — .card-subtle and .card-translucent combine — which is why translucent is a boolean of its own and not a second variant value.
  • New: layout="row" lays a card out along the inline axis, with <CCardImage orientation="start" | "end"> for an image cap on either side. It replaces the <CRow className="g-0"> + <CCol md={4}> recipe, which needed a wrapper per section and drew no divider between them.

CChip, CChipSet, CChipInput

  • Breaking CChipInput carries its chips in a hidden <textarea>, submitted only once you name it. It took a generated name when the prop was not set, so a form carried a key nobody chose; the field is now named only by the prop. It is a <textarea> instead of an <input type="hidden">, because the browser leaves a hidden input out of validation, and the submitted value is the same, the chips separated by commas. With it CChipInput takes required, validationState, onValidationStateChange and the feedback props, blocks the submit while it is empty and required, and a native form reset puts back the chips it started with. A disabled CChipInput is no longer submitted, like any disabled field. See Validation.

  • Breaking CChip drops active; selected is the one prop that renders .active. On a selectable or filter chip selected keeps its controlled toggle semantics (with aria-pressed/aria-selected and the filter check icon); on any other chip selected={true} only renders the .active class — no toggle, no ARIA state — which is what active did. Replace active with selected.

  • Breaking The chips expose real roles. A selectable CChipSet is a horizontal role="listbox" (with aria-multiselectable outside single mode) whose chips are role="option" carrying aria-selected; a non-selectable set is a role="group". A standalone selectable CChip is a toggle button (role="button") and reflects its state through aria-pressed instead of aria-selected.

  • Breaking aria-disabled is stamped only on a chip that carries a role. On a role-less chip the attribute was invalid; the disabled class and the removed interactivity carry the state.

  • Breaking CChipInput no longer stamps aria-disabled/aria-readonly on its container. The container is a generic element, so those attributes were invalid on it; the native disabled/readonly states of the inner input carry the semantics.

  • Adding and removing chips is announced. CChipSet and CChipInput announce each change through announce(); the wording is configurable through ariaAddedAnnouncement and ariaRemovedAnnouncement.

  • Every remove button names its chip. CChip and CChipSet named each remove button Remove, so a set read the same name for every chip; the button is Remove <text> now, as CChipInput and CMultiSelect already did. ariaRemoveLabel (and ariaChipRemoveLabel on CMultiSelect) 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 label that already spelled the text out, such as ariaRemoveLabel={`Remove ${chip}`}, now repeats it; drop it or use {label}.

  • A selectable chip hides its remove affordance. A chip with a role — a selectable chip, which is a toggle button, or an option in a selectable set — gets a <span aria-hidden="true"> instead of a <button> inside it, so its name is its text and a control is no longer nested in a button. Backspace and Delete remove it from the keyboard, the affordance from the pointer.

  • A chip set is one tab stop. CChipSet and CChipInput gave every chip tabIndex={0}, so Tab walked through each chip. The chip that last had focus is the tab stop now, the first chip until then, and the arrow keys move between the rest, chips without a value included. CChip computes its tabIndex from selectable, removable and disabled; a tabIndex passed to it is ignored.

  • CChipInput keeps focus among the chips after a removal. Removing the focused chip sent focus to the text input; it now moves to the next chip, else the previous one, and reaches the text input once no chip is left, the rule CChipSet follows.

CCloseButton

  • Breaking The white and dark props are gone. The glyph is drawn with a CSS mask and takes the button’s inherited color, so on any surface that paints its own text — a .text-bg-inverse toast, a dark navbar’s offcanvas — it is already the right color, and neither prop has anything left to do.

    white rendered .btn-close-white, which v6 removed. dark stamped data-coreui-theme="dark" on the button, which used to match a rule that inverted the glyph through a filter; that rule went with the mask, and what the attribute still sets — the shadow strength and color-scheme — reaches nothing the close button draws. To flip a region, put the attribute on the section and give it a surface (.bg-body.text-body); the button follows the text it inherits.

CCollapse

  • The stylesheet owns the animation where the browser can interpolate a size to auto. CCollapse no longer writes an inline height/width there — driving the size from React as well would run the same expansion from both ends. Browsers without interpolate-size keep the measured path, and both read the duration and easing from --cui-transition-collapse-duration / -timing instead of a hardcoded 350 ms.
  • Breaking .collapse stays on the element for the whole run. It used to be dropped while .collapsing was on, which in v6 means the element misses interpolate-size and the transition the stylesheet puts on .collapse — so the CSS path would not animate at all. .show also lands at the start of the opening run rather than at its end, because the stylesheet reads it as the open state and animates towards it. Custom CSS that keyed off the old combinations needs updating.
  • New: hiddenUntilFound hides the collapsed area with hidden="until-found" rather than taking it out of the layout, so find-in-page searches it and the browser expands it on a match. onShow fires when that happens — set visible from it, or the stylesheet hides the content the reader was just sent to. Off by default; the attribute is left off entirely below Chrome 102 / Firefox 148 / Safari 26.2.

CConditionalPortal

  • CConditionalPortal resolves its target while rendering. It used to render its children in place for one commit and then teleport them, which remounted the subtree and dropped any focus those children had taken.

CContextMenu

  • New component. <CContextMenu> is the trigger area: a right-click, or Shift+F10 / the Menu key while it has focus, opens the <CMenuContent> inside it at the pointer and suppresses the browser’s own menu. It is a CMenu positioned against the pointer with position: fixed, so items, submenus, keyboard navigation and autoClose are the Menu’s. Focus moves into the menu on open and returns to the area on close; the page does not scroll while the menu is open. A row’s own onContextMenu handler runs first — call preventDefault() there to keep both menus closed.

CDateInput, CDateRangeInput, CTimeInput

  • Breaking A required field blocks the form. The value moved from an <input type="hidden">, which the browser leaves out of validation, to a <textarea> laid invisibly over the field, 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 native input and change events when the date changes, which onInput on a form sees and React’s onChange does not. This applies to the 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.
  • Breaking A given state is a verdict on one value. validationState="invalid" now blocks the submit, with the text of the message after the field, until it is cleared. The user’s next change of the value and a native form reset call onValidationStateChange(undefined) before the change is reported, so pass your state setter there; a value your code sets keeps the state. Without a given state the field shows what a validation reports and, in a form with validate, the :user-invalid and :user-valid states of its value field, as a native field does. aria-invalid sits on the sections, not on the group, and an aria-invalid you pass is not read as a state. This applies to the pickers and to both ends of a date range as well.

CDatePicker, CDateRangePicker, CTimePicker

The pickers are no longer monoliths. Each is a composition of a section field, a calendar or time selection, and an anchored panel — which is what removes most of the props below: the field and the panel are now the same primitives you can use directly.

  • Breaking CDatePicker is its own component. In v5 it was a thin wrapper over CDateRangePicker and inherited its props; it now has its own, single-date API (date, defaultDate, onDateChange).

  • Breaking The button-configuration props are gone — todayButton, todayButtonColor, todayButtonSize, todayButtonVariant, cancelButton*, confirmButton* and rangesButtons*. Project the content instead: the footer prop takes a node, or a function receiving the picker’s context, so a button you render yourself can act on the picker and read its state. The same context is available to any descendant through useDatePicker(), useDateRangePicker() and useTimePicker().

    <CDatePicker
      footer={({ today, clear, close, isDateSelectable }) => (
        <>
          <CButton disabled={!isDateSelectable(new Date())} onClick={() => isDateSelectable(new Date()) && today()} size="sm">Today</CButton>
          <CButton onClick={clear} size="sm">Clear</CButton>
          <CButton onClick={close} size="sm">OK</CButton>
        </>
      )}
    />
  • Breaking CDateRangePicker: ranges is a projected region too. It takes a node or a function receiving the context, so a preset button calls setRange with its own dates; rangesButtonsColor/-Size/-Variant are gone with it. The controlled range is startDate/endDate (with defaultStartDate/defaultEndDate for the uncontrolled mode), and the two fields take startName/endName.

  • A picker action shows its date in the calendar. setRange, reset, setDate and today from the picker’s context turn the calendar to the date they set (a range’s start, else its end) in the view it is in, also when the value does not change and over a calendarDate you pass, so a preset clicked again after browsing brings its range back. clear and an invalid date leave the calendar where it is; a typed date, a picked time and the dates you pass move it as before.

  • Breaking A date field is submitted only once you name it. CDatePicker and CDateRangePicker built the name of their inputs from id when name was absent, so <CDatePicker id="delivery" /> posted delivery-date and the range posted delivery-start-date / delivery-end-date. That fallback was deprecated in 5.3.0 and is gone: an id names the element for a label, it does not put the value in the payload. Pass name, or startName/endName, for the field to reach the server.

  • Breaking The field props are gone: inputDateFormat, inputOnChangeDelay, inputReadOnly, placeholder, separator. The value is a mask now — pass format (or a section layout) and, for the placeholder text, one prop per section — dayPlaceholder, monthPlaceholder, yearPlaceholder and the rest. The range placeholder also named the two fields; CDateRangePicker names them with ariaStartLabel and ariaEndLabel now (“Start date” and “End date” by default), or with floatingLabels when set. Use readOnly for a read-only control and separatorIcon for the artwork between the two range fields.

  • Kept. timepicker still adds the time half to CDatePicker, 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. hours, minutes and seconds narrow what the time half offers.

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

  • Breaking 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: hours={[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 also speaks the locale, 오전 and 오후 in Korean, where it read “AM” and “PM”. 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.

  • Breaking The time picker’s roll classes lose the roll infix, and the date picker’s time controls carry its own name. .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 was always present on .time-picker-body, so it is gone rather than renamed, and --cui-time-picker-body-padding went with it. .time-picker-inline-icon and .time-picker-inline-select are .date-picker-time-icon and .date-picker-time-select, with --cui-date-picker-time-* tokens, because they belong to the date picker’s panel. Custom CSS written against any of these names needs the new one.

  • Breaking CTimePicker: variant is gone. Each picker has one time rendering — the time picker shows the roll, and the date picker’s time half shows the selects, which is what v5 rendered inside a date picker with time. The selects ship in the date picker stylesheet, so a page that only uses the time picker no longer downloads them.

  • Breaking The time selection is one tab stop. Tab reaches the body once, Arrow Left and Arrow Right move between the hour, minute, second and AM/PM parts and stop at the ends, and Arrow Up / Arrow Down change the focused value. Both renderings answer the same keys.

  • Breaking CTimePicker: ampm is gone. The hour cycle follows the field’s mask when it names one (HH vs hh a) and the locale otherwise.

  • Breaking closeOnSelect and previewDateOnHover are gone. A complete selection closes the panel unless a footer projects actions the user still needs.

  • Breaking indicator is gone. The button that opens the panel is always rendered; pickerIcon replaces its artwork. The button that clears the value is the one you can switch off, with cleaner.

  • Breaking The v1 shell class names are gone: date-picker-input, date-picker-input-wrapper, date-picker-input-preview, date-picker-cleaner, time-picker-input, time-picker-cleaner, and the -dropdown panels are now .date-picker-popup / .time-picker-popup.

  • The panel is rendered through a portal by default (portal), into container when given. It is built on the first open, not on mount.

  • New props across the family: cleaner and cleanerIcon, pickerIcon, disabled, visible, onShow/onHide, size, and the accessible labels ariaCleanerLabel / ariaPickerLabel.

  • Pickers open on the platform’s dropdown keys. Alt+ArrowDown and F4 open the panel and move focus into it — onto the selected date, else today or the minDate / maxDate today falls outside of, also when that date cannot be picked. The field’s own arrows keep editing the value, as <input type="date"> does. Opening with the mouse moves focus in too.

CDropdown

  • Placements are mirrored once now, by Floating UI. The -start/-end half of a placement is logical — Floating UI resolves it from the writing direction of the floating element — and the library was mirroring it a second time, which cancelled it. In a right-to-left context that moves every placement whose suffix you set: alignment="end" now hangs from the inline end, and an explicit placement="bottom-start" opens from the right edge instead of the left. The sides are the other way round: Floating UI never mirrors left/right, so a logical start/end side still maps to the physical one the direction asks for and dropend/dropstart still swap. Also in this change: a dropup aligned to the end keeps its side (it resolved to bottom-end before), direction="center" resolves to bottom rather than bottom-start, a submenu in a right-to-left context anchors to the top of its trigger rather than the bottom, and useDropdownWithFloating asks Floating UI for a plain bottom-start. CMenu, CSubmenu, CContextMenu and the picker panels all resolve a placement the same way. CTooltip and CPopover keep mirroring a physical left/right as a v5 compatibility fallback, which is removed in v7.
  • A panel sent through a portal carries the direction of its trigger. Out in document.body it no longer inherits the direction that resolves the suffix, so it would have been laid out as left-to-right while its trigger sat in a dir="rtl" subtree. A dir you set yourself on the panel is left alone.
  • CDropdown is now a compatibility layer over CMenu. The v5 markup, class names, props and callbacks keep working — the two share one engine underneath, so the dropdown also gained Escape closing regardless of autoClose. Deprecation notice: CDropdown may be removed in v7 — prefer CMenu for new work.
  • Breaking popperConfig → floatingConfig, see Dependencies.

CFocusTrap

  • CFocusTrap’s additionalContainer accepts an element, not only a ref. A ref is read once, when the trap activates, which is too early for a panel that is built on open.

CFooter

  • Breaking CFooter is removed. It rendered a .footer class whose nine declarations utilities already cover, so a footer is plain markup now: <footer className="d-flex flex-wrap align-items-center justify-content-between bg-3 border-top px-5 py-3">. The min-height: 3rem floor has no utility — set it yourself where it matters, or pad with py-4.
  • Breaking The position prop goes with it. Use .sticky-bottom in place of position="sticky" (it pins the same way, on --cui-z-sticky rather than --cui-z-fixed) and .fixed-bottom in place of position="fixed", which emitted a .footer-fixed class the PRO stylesheet has never defined.

CForm

  • validate with noValidate validates the form on submit. <CForm> stops an invalid submit (onSubmit runs only for a submit that goes through), marks the invalid controls with .is-invalid and aria-invalid, links the message of each to it with aria-describedby while it stays invalid, and moves focus to the first. A hand-written onSubmit that only called checkValidity() and preventDefault() can go. The new onValidate callback runs first; call preventDefault() on its argument to handle the result yourself.

CFormCheck

  • Breaking hitArea is removed. It rendered the label as a stretched-link so a whole list group item would toggle the box; the list group item can be the label itself now — <CListGroupItem as="label"><CCheckbox className="me-1" /> Text </CListGroupItem> — which is what the stylesheet is written for.

CFormFloating

  • Breaking The placeholder shows once the field has focus. It used to stay transparent in every state; it now appears under the floated label while the field, or the control inside a field component, has focus, and stays hidden while the label rests in the field. A plainText control keeps it hidden.
  • New: always on <CFormFloating> and floatingLabelAlways on every form control that renders a floating label keep the label floated and the placeholder showing while the field is empty. See Always floating.

CFormLabel

  • Breaking customClassName is removed. It was the one prop in the library that replaced className instead of adding to it, and only CFormCheck and CFormSwitch used it internally to render a label without .form-label; they render a plain <label> now. Pass className for extra classes; a label without the base class is a plain <label> element.

CFormRange

  • 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 color.
  • Breaking <CFormRange> renders a wrapper. The input sits inside <div class="form-range"> as .form-range-input; className, style, theme, dir and hidden land on the wrapper, which carries the tokens, while every other prop still reaches the input and ref still points at it. A label without an id gets a generated one, so the label names the input. The component writes --cui-range-fill (0–1) from the value, so the track fills up to the thumb, a shade lighter than it, in every browser. An uncontrolled range seeds from defaultValue (the native midpoint without it).
  • tooltips and tooltipsFormat show the value above the thumb, on hover, drag and focus or "always"; ticks renders tick marks from an array of values, labels or { value, label, className, style } objects, labels beneath. Both follow the vanilla Range plugin, which also reads a <datalist>, and share names with <CRangeSlider>. onChange gets the value as a number in its second argument, onChange(event, value). Several handles, distance, clickable ticks and a vertical orientation are what <CRangeSlider> adds.

CHeader

  • Breaking position renders position utilities. position="sticky" emits .sticky-top in place of .header-sticky, which the stylesheet no longer defines — the two rules were identical, so the header pins exactly as before. position="fixed" emits .fixed-top; the .header-fixed class it used to render was never defined anywhere, so that value had no effect at all. For the bottom variants, or a responsive one like .sticky-md-top, pass the utility through className instead.

CInputGroup

  • Breaking Validation feedback moves out of the group. The .has-validation class is gone, and a group keeps its corner logic by having the control as its last child — a CFormFeedback left inside squares off the end of the control it belongs to. Wrap the group in a CFormField and put the feedback after it, so the message renders under the whole frame. Inside a group that means a standalone CFormFeedback rather than the feedback, feedbackValid or feedbackInvalid props, which render it next to the control.

    <CFormField>
      <CInputGroup>
        <CInputGroupText>@</CInputGroupText>
        <CFormInput validationState="invalid" required />
      </CInputGroup>
      <CFormFeedback validationState="invalid">Please choose a username.</CFormFeedback>
    </CFormField>

CLoadingButton

  • spinnerType is deprecated in favour of spinnerVariant — the same word CSpinner uses for border / grow; the old name loses to the new one. onClick receives the click event, as on CButton, instead of nothing.
  • The spinner takes the button’s font size. It no longer asks CSpinner for size="sm" — a spinner is 1em square in v6, so inside a button it already lands where the small size used to put it. Custom CSS targeting .btn-loading-spinner.spinner-border-sm or .spinner-grow-sm has nothing to match; the sizing classes are gone from that element.
  • Breaking disabledOnLoading no longer renders 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 tests 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 prop, then its name again. The spinner no longer renders role="status" or the “Loading…” text that Safari read on an idle button; while loading it is a role="img" named by ariaLoadingLabel.

CMenu

  • New: custom on CMenuToggle. With it the single child element receives the toggle behaviour — aria-expanded, aria-haspopup, the click handler and the ref — and keeps its own markup and classes, instead of the toggle rendering a CButton. CDropdownToggle has carried the same prop since v4; this closes the gap in the component that replaces it.

CModal, COffcanvas

  • Breaking Both render a native <dialog>. The browser provides the top layer, the ::backdrop, focus trapping, background inertness and the dialog ARIA semantics, so the markup drops role, aria-modal, aria-hidden and tabIndex={-1}, and the component no longer renders a backdrop, a focus trap or a portal of its own.
  • Breaking CModalDialog and CModalContent are removed. .modal > .modal-dialog > .modal-content flattens to a single <dialog className="modal"> holding the header, body and footer directly. CModal rendered both for you, so markup written against the components is unchanged; custom CSS targeting those two classes is not.
  • Breaking portal and container are gone. They existed to lift the modal out of a stacking context. A dialog opened with showModal() renders in the top layer, above every stacking context on the page, so there is nothing left to escape.
  • Breaking focus is gone — a modal dialog always traps focus, and the option could only have turned off something the browser does.
  • Breaking alignment is gone — the browser centers a modal dialog. alignment="top" has no replacement; a dialog that must sit at the top is a positioning override in your own CSS.
  • Breaking .modal-dialog-scrollable is .modal-scrollable, and .modal-backdrop / .offcanvas-backdrop are gone with the elements they styled — the backdrop is the ::backdrop pseudo-element, themed through --cui-modal-backdrop-* and --cui-offcanvas-backdrop-*.
  • Breaking Animation opt-out is a class now. transition={false} renders .modal-instant / .offcanvas-instant instead of dropping .fade; the offcanvas .showing state class is replaced by the native open attribute, and .hiding carries the exit.
  • New: modal={false} on CModal opens the dialog non-modally, leaving the rest of the page interactive. New: animation on COffcanvas.
  • Scroll locking moved to the root element — dialog-open with scrollbar-gutter: stable, instead of inline overflow and padding-right on the body.

CNavGroup

  • The stylesheet owns the group animation where the browser can interpolate a size to auto. CNavGroup no longer writes an inline height/display on .nav-group-items; the run is marked with .collapsing and the open state with .show on the group, and the duration and easing come from --cui-sidebar-nav-group-items-transition-*. Browsers without interpolate-size keep the measured path. Custom CSS keyed on the inline height needs updating.
  • A group that renders open no longer plays the opening animation on mount — defaultShow, a controlled show and an active nav link all reveal the branch straight away.

COneTimePassword

  • Breaking Only COneTimePassword’s hidden input carries the field’s name. Each COneTimePasswordInput was given <name>-<index> on top of it, so a six-slot field submitted seven keys where the prop is documented as the name of the hidden input. The slots are unnamed now, and a name you pass to a slot is left alone instead of being overwritten.
  • Breaking The code is validated as one value. It moved from an <input type="hidden">, which the browser leaves out of validation, to a text input laid invisibly over the group, with tabIndex={-1}, 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. Once the browser reports the code, every slot carries aria-invalid="true" and the error message while the code stays invalid, as it already did for validationState="invalid", and the stylesheet shows the message after the group, 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), and a data-coreui-invalid-feedback written on the slots goes on COneTimePassword.
  • Breaking COneTimePassword blocks the submit while validationState (or the deprecated invalid) is 'invalid', and asks to drop the state with onValidationStateChange(undefined) when the user changes the code or the form is reset. The state class moved from each COneTimePasswordInput to the .form-otp group, which the stylesheet colours as a whole, so CSS or tests keyed on .form-otp-control.is-invalid need .form-otp.is-invalid. An aria-invalid passed to the component or to a slot is no longer put on the group or the slot, the hidden input sends input and change whenever the code changes, also through value, and a native form reset puts back the code given by defaultValue. See Validation.
  • Breaking The readOnly prop is removed, and every slot is editable. A code the user cannot type has no use in a form, so a readOnly on COneTimePassword or on a COneTimePasswordInput no longer makes a slot read-only; use disabled to show a code the user cannot change.

CPasswordInput

  • Breaking Renamed ariaLabelToggler to ariaToggleLabel.
  • Breaking The toggle icon is rendered by the component instead of being drawn by CSS, and is replaceable through the new showIcon / hideIcon props. .form-password-action-icon is gone, the frame is .form-control-group and the button is .form-control-action.

CPlaceholder

  • animation="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 animation="pulse" now; animation="glow" stays as its alias. The prop still has no default. The keyframes are renamed to placeholder-pulse with it. With reduced motion the wave stops and drops its mask.
  • 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 taller, and .75em, 1em and 1.75em for the xs, sm and lg sizes (were .6em, .8em, 1.2em). Each placeholder has a --cui-radius-3 corner; a CButton, CAvatar, CBadge or CChip with the placeholder class keeps its own height and corners. The wave reads its own --cui-placeholder-wave-opacity-max and -min.

CPopover, CTooltip

  • onShow and onHide fire when the tip starts showing or hiding, after delay and only when the visibility actually changes. onShow used to fire on the trigger gesture, before delay.show, and again for a second trigger on a visible tip, while onHide also fired after a hover that ended before the tip appeared. onShowChange reports the requests; a deprecated visible kept in step through onShow and onHide belongs on show and onShowChange.
  • New: onShown / onHidden, fired once the entry or exit transition has finished.

CProgress, CProgressBar

  • Breaking thin is removed from CProgress. It rendered .progress-thin, which v6 does not ship; set height for a slimmer bar.
  • Breaking color no longer takes a gradient. v6 removes the bg-*-gradient utilities the value mapped to, so the Gradients union is gone from the prop’s type. Reach for .bg-gradient alongside the color if you want the sheen.

CRangeSlider

  • Breaking The slider renders the markup of the range. It is a <div class="form-range"> with one native .form-range-input per handle; className, style, theme, dir, hidden, role, aria-label, aria-labelledby and the ref land on that div, and other input attributes, such as id (on the first handle) and aria-describedby, reach the handles. The classes follow: .range-slider → .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, .range-slider-track and the .disabled class are gone. The handles no longer carry role="slider", aria-valuemin, aria-valuemax or aria-valuenow: the native inputs expose them.
  • Breaking The handles and the track read the range’s tokens. --cui-range-slider-* is gone: set --cui-range-thumb-*, --cui-range-track-*, --cui-range-track-fill-bg (was -track-in-range-bg), --cui-range-track-disabled-bg (was -disabled-track-in-range-bg) and --cui-range-tick-label-color and -font-size (were --cui-range-slider-label-*) on the slider. The label padding has no counterpart, the tooltips sit like the range’s without --cui-range-slider-tooltip-margin-*, and a vertical slider’s height is --cui-range-vertical-height and its track width --cui-range-track-height.
  • Breaking ariaLabels is ariaLabel, an array with one name per handle or a function (index, total) => string.
  • The slider is a group named after the field. The .form-range gets role="group" with the aria-label or aria-labelledby you pass, so a screen reader announces the field when the focus enters it. With several handles, each handle carries only its own name (“Minimum value”); a single handle takes the name of the group. aria-describedby describes every handle, and a role you pass replaces group on the slider.
  • The slider takes a validation state. validationState shows on the slider and on the handles, and 'invalid' blocks the submit through the first handle until the user moves a handle or the form is reset; the slider calls onValidationStateChange(undefined) then. See Validation.
  • Breaking labels is ticks, clickableLabels is clickableTicks. An entry is a number (a tick without text), a string (a label placed by its position), or { value, label, className, style }; a bare element is no longer an entry, pass it as { label: <strong>High</strong> }.
  • Interaction follows the native control. Pressing a handle no longer moves it; a press on the track or a clickable tick moves the nearest handle and it follows the pointer (mouse, touch or pen); a form reset restores defaultValue. A controlled slider whose parent ignores onChange keeps the handle on its value instead of letting it follow the pointer until the drag ends.
  • tooltips takes "always". true shows a handle’s tooltip on hover, drag and focus; "always" keeps them on.

CRating

  • 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 (the first star while no star is checked), 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.
  • A read-only rating shows no tooltips.
  • The rating takes a validation state and can be required. validationState colours the focus ring, and 'invalid' sets aria-invalid and blocks the submit until you drop the state; the rating asks for that with onValidationStateChange(undefined) when the user picks or clears a star or the form is reset. required makes every radio required. See Validation.
  • 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.

CSidebar

  • onVisibleChange is deprecated in favour of onInViewportChange. The callback reports whether the sidebar is on screen — it is off-canvas on narrow viewports — not the show state, and the old name read like the latter’s twin. Both fire while the old name is set.
  • colorScheme="light" joins colorScheme="dark". Either value pins the sidebar to one scheme whatever the page uses, so a dark navigation column can sit on a light page.
  • Theme variants. className="theme-{color}" paints the surface, text, icons and the hover/active links from that colour’s slots.
  • 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. Slightly stronger than before on a light sidebar, slightly weaker on a dark one.

CSmartTable

  • Breaking The props are renamed. Every filter prop says which filter it means, the column configuration drops the v3 underscores and the sorter icons take the names the other data components use:

    v5v6
    elementCoverloadingContent
    itemsNumbertotalItems
    onSelectedItemsChangeonSelectedChange
    sortingIcon, sortingIconAscending, sortingIconDescendingsortNeutralIcon, sortAscendingIcon, sortDescendingIcon
    tableFilter, tableFilterValue, defaultTableFilterValue, onTableFilterChangeglobalFilter, globalFilterValue, defaultGlobalFilterValue, onGlobalFilterChange
    tableFilterLabel, tableFilterPlaceholderglobalFilterLabel, globalFilterPlaceholder
    columnSorter={{ resetable: true }}columnSorter={{ resettable: true }}
    Column._style, Column._propsColumn.headerStyle, Column.headerProps
    Column._colStyle, Column._colPropsColumn.cellStyle, Column.cellProps
    Column.sorter: falseColumn.sortable: false (sorter is the comparator only)
    Column.filter: falseColumn.filterable: false (filter is the custom filter only)
    FooterItem._propsFooterItem.props
    onSelectAll— (onSelectedChange carries the same information)
  • Breaking A column group is { label, children }. The group label moves from group to label and a group no longer needs a key: { group: 'Address', key: 'address', children: [...] } becomes { label: 'Address', children: [...] }.

  • Breaking One external prop replaces the six external options. columnFilter, tableFilter, columnSorter, pagination, itemsPerPageSelect and selectAll each carried an external flag; external on the table says the same thing once — the items arrive already filtered, sorted and sliced, so the table renders them as they are and only reports the requested state. Pair it with totalItems so it can compute the number of pages. Where the option only meant “let the parent own this piece of state”, pass the controlled prop instead.

  • Breaking SorterValue is { column, direction } and the sorter value is always an array. state: 'asc' | 'desc' | 0 becomes direction: 'asc' | 'desc', with an absent entry standing for “not sorted”, and sorterValue, defaultSorterValue and the onSorterChange payload are SorterValue[] whether or not columnSorter={{ multiple: true }} is set — no more Array.isArray at the call site.

  • Breaking Item._selected is removed. Seed the selection with defaultSelected, or own it with selected — the flag in the data was a second source of truth and the two drifted apart.

  • The selection checkboxes and the cleaner button have accessible names. ariaSelectAllLabel ('Select all rows'), ariaSelectRowLabel ('Select row') and ariaCleanerLabel ('Clear filters and sorting') are read by screen readers; before, none of the three controls had a name at all.

  • itemKey identifies a row. With it set, selection and React keys compare item[itemKey] instead of the whole object, so a selected row survives a re-fetch of the data.

  • cleanerIcon replaces the cleaner button’s icon, the way the sorter icons already could.

  • The selection checkboxes render CCheckbox. The select-all control in the head and the per-row control in the body are <input class="check"> now, not .form-check-input, so custom CSS keyed on the old class needs updating.

  • The chrome is composable. CSmartTable renders the layout it always did as long as it has no children; pass children and it renders them instead, so CSmartTableFilter, CSmartTableCleaner, CSmartTableContent, CSmartTablePagination and CSmartTableItemsPerPage go wherever you put them, next to your own markup. useSmartTableContext() reads the same state — the items, the page, the filters, the sorter and the selection — so a part of your own is a component that calls it.

  • useSmartTable(options) runs the table without any of its markup. The whole state — the filters, the sorter, the pages, the selection with isSelected, toggle, toggleAll and selectedAll — is a hook you can call with the state props of CSmartTable and render however you like, tables included or not. CSmartTable itself is that hook plus the layout.

CSpinner

  • visuallyHiddenLabel is deprecated in favour of ariaLoadingLabel, which works as ariaLoadingLabel does on CLoadingButton: the text defaults to 'Loading' instead of 'Loading...', and an empty string hides the spinner from screen readers. visuallyHiddenLabel still sets the text while ariaLoadingLabel is unset.

  • Spinners are sized in em, not rem. A spinner is 1em square now, so it takes the font size of whatever it sits in instead of being a fixed 32 px block. Inside a button or a heading it lands at the size you previously had to ask for with size="sm"; standalone it is half of what it was. Set --cui-spinner-width / --cui-spinner-height to go back to a fixed size.

  • size="sm" renders .spinner-sm, not .spinner-border-sm / .spinner-grow-sm. The prop is unchanged; only custom CSS targeting the old per-type classes needs updating.

  • .spinner-grow eases with ease-in-out instead of pulsing linearly. Override --cui-spinner-animation-timing-function to change either spinner’s easing.

  • .spinner-rotate is new. Pass it through className on any element to spin your own artwork without inheriting the bordered circle.

CStepper

  • Breaking --cui-stepper-step-indicator-active-bg is removed. The active step’s indicator background is a 5% tint of the stepper’s theme color, or of --cui-stepper-step-indicator-active-color without one. To give it another background, set --cui-stepper-step-indicator-bg on .stepper-step-button.active in your stylesheet.

  • Breaking activeStepIndex and defaultActiveStepIndex are removed. Deprecated since 5.16.0 for their 0-based indexing; use activeStepNumber / defaultActiveStepNumber, which count from 1.

  • A failed step moves focus to the first invalid control, and the panes are no longer live regions. With a noValidate form the invalid controls also get aria-invalid, and the message is read with the field: the stepper adds the .invalid-feedback that follows the control, which covers feedbackInvalid, or the elements named in data-coreui-invalid-feedback, to its aria-describedby. 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().

  • onStepValidationComplete runs before the stepper decides. A validity it sets with setCustomValidity() blocks the step, and isValid tells whether every control passed before it ran. A noValidate step form is marked each time the step is validated, also when it passes, so data-coreui-validate="valid" now marks the valid controls of a passing step with .is-valid.

  • A non-linear stepper validates the step being left. With linear={false}, clicking a later step used to skip validation; it now runs it like next() does and stays put while the step is invalid. Going back is never blocked. Add validation={false} for the old behaviour.

CTable

  • Breaking caption is the caption and captionTop is a boolean. caption="top" no longer carries a magic value and captionTop no longer carries text: <CTable caption="top"><CTableCaption>…</CTableCaption></CTable> becomes <CTable captionTop>…</CTable>, and <CTable captionTop="List of users" /> becomes <CTable caption="List of users" captionTop />.
  • New: card wraps the table in a card’s border and corner radius, clips its cells and drops its bottom margin and the borders that would stack on the frame. With responsive the class lands on the same wrapper. See Card-like tables.

CToast, CToaster

  • Breaking CToaster.push is gone; toasts come from a store. createToaster({ placement, limit, timeout }) builds the store outside the React tree and <CToaster toaster={toaster} /> renders it: add(options) returns the id (an existing id updates in place and bumps updateKey, a closing one is replaced), update(id, options | fn(prev)) rewrites a toast, close(id?) hides one or all, promise(p, { loading, success, error }) follows an asynchronous operation. A toast is options — title, description, theme, timeout (0 = sticky), priority ('normal' or 'high', the polite or the assertive live region), dismissible, action, render — instead of a <CToast> element. limit keeps the newest toasts visible and leaves the oldest in the DOM as inert and hidden. useToaster() returns the store inside the container. <CToaster> without toaster stays a container for <CToast> children.

    // v5
    const [toast, addToast] = useState()
    <CButton onClick={() => addToast(<CToast><CToastBody>Saved</CToastBody></CToast>)} />
    <CToaster placement="top-end" push={toast} />
    
    // v6
    const toaster = createToaster({ placement: 'top-end' })
    <CButton onClick={() => toaster.add({ description: 'Saved' })} />
    <CToaster toaster={toaster} />
  • Breaking CToast drops index and innerKey, and onClose / onShow fire without an argument — they were the plumbing of push, and a toast rendered by the store is identified by its key and data-coreui-toast-id.

  • Placement is one class. <CToaster placement> renders .toast-container-{placement} in place of the position-fixed top-0 end-0 utility stack; the edge inset comes from --cui-toast-container-inset. The container is a region landmark named Notifications.

  • The store carries the rest of the behavior. ariaLabel names the landmark, enter and leave pick the edges a toast slides in from and out to, pauseOnHover (on by default) stops every timer while the pointer or focus is inside the container, and stack: true piles the toasts up newest-in-front and spreads them out on hover. <CToaster portal={false}> keeps the container where it is written instead of portalling it onto document.body.

  • Announcements moved off the toast. A toast rendered by the toaster carries no role, aria-live or aria-atomic; every add() and update() passes the string parts of title and description to announce(), which reads them at the priority priority selects. A standalone <CToast> keeps its own role="alert" defaults.

  • New: translucent renders .toast-translucent — the background shows through the toast, blurred, and the header thins out with it.

  • The fade moved to CSS. The toast no longer renders the fade and showing helper classes and no longer drives its own opacity — show alone drives the transition, with @starting-style supplying the state to animate from and a discrete display transition keeping the toast laid out until the fade-out finishes. animation={false} now renders .toast-instant instead of dropping fade, and finishes both directions immediately. The prop itself is unchanged; only custom CSS targeting .toast.fade or .toast.showing needs updating. Tune the motion with --cui-toast-transition-duration and --cui-toast-transition-timing.

CWidgetStatsA – CWidgetStatsF

  • Breaking The six widget components are removed. They were the one family in the library with no stylesheet of their own: each rendered a CCard and a fixed stack of utility classes, so they carried a layout decision rather than styling you cannot write in place. Compose the same markup directly, which also lets a widget diverge from the six shapes we happened to ship:
// Before
<CWidgetStatsF color="primary" icon={<CIcon icon={cilSettings} height={24} />} title="Users" value="87.500" />

// After
<CCard>
  <CCardBody className="d-flex flex-row align-items-center">
    <div className="me-4 fg-white bg-primary p-4">
      <CIcon icon={cilSettings} height={24} />
    </div>
    <div>
      <div className="fs-6 fw-semibold fg-primary">87.500</div>
      <div className="fg-2 text-uppercase fw-semibold small">Users</div>
    </div>
  </CCardBody>
</CCard>
  • The six shapes live on as blocks. Every one of them is on that page as markup to copy, with the names they go by now: CWidgetStatsA is the stat with a chart, CWidgetStatsB the stat with a progress bar, CWidgetStatsC the one with an icon and a progress bar, CWidgetStatsD the stat group, CWidgetStatsE the compact stat and CWidgetStatsF the stat with an icon.
  • Three of them did not survive the v6 stylesheet unchanged, which is the other half of the reason they are going. CCardBody became a flex column in v6, so CWidgetStatsA stacked its menu under the figure instead of putting it in the corner, CWidgetStatsF stacked its icon above the text, and CWidgetStatsD stacked its values instead of laying them in a row. The blocks carry the flex-row and w-100 those layouts now need.