Changelog

Notable changes to Colorffy CSS and Colorffy UI, newest first.

Notable changes to @colorffy/css and @colorffy/ui, newest first. Anything that needs you to edit your own markup is written up in full in the migration guide — those entries link there instead of repeating it.

3.0.0

Added

  • Density modes. data-density="compact | comfortable | spacious" on <html> or any element tightens or loosens the spacing and the height of fields and buttons inside it; the nearest one wins, and touch screens keep comfortable heights in compact. Where container style queries are supported, the --cffy-density custom property selects a mode from CSS as well. Two new token scales carry the heights, --cffy-field-height-sm|md|lg and --cffy-control-height-sm|md|lg, and the input, button and calendar day hooks default to them; comfortable matches the previous sizes. See Density.
  • Spacing tokens for the large steps. --cffy-space-28, -36 and -40 (behind gap-7, gap-9 and gap-10) and --cffy-space-72, -96, -120, -144 and -192 (behind spacing steps 6–10) join the scale. They follow the unit and the density like the other steps, and custom CSS can use them too.
  • Navbar CSS variables. .navbar reads 25 public variables, from --cffy-navbar-bg-color and --cffy-navbar-radius to the link states (--cffy-navbar-link-hover-color, --cffy-navbar-link-active-bg-color, …). Set them on :root or on a wrapper to restyle every navbar inside it; nav-island, nav-transparent and the stuck state keep their own values. See Navbar → CSS variables.
  • Popover menu CSS variables. .popover-menu-container reads 16 public variables for the panel (--cffy-popover-menu-bg-color, -radius, -shadow, -min-width, …) and its items (--cffy-popover-menu-item-color, -hover-*, -active-*). Destructive items keep their danger color. See Popover menu → CSS variables.
  • Dropdown CSS variables. Every dropdown panel reads 17 public variables (--cffy-dropdown-bg-color, -radius, -min-width, …) and its items (--cffy-dropdown-item-color, -hover-*, -active-*, padding and radius). The easing and duration hooks also reach the mobile bottom sheet, whose variant no longer repeats the base timing. See Button menu → CSS variables.
  • Dialog CSS variables. Dialogs read 18 public variables (--cffy-dialog-bg-color, -radius, -padding, -backdrop-color, -backdrop-blur, title, subtitle and icon colors, …). The easing and duration now reach modals and side sheets: they re-declared the same overshoot curve and 600ms over a base easing that never ran, and the side sheet repeated the base radius, so those duplicates are gone. See Modal → CSS variables.
  • Sidebar CSS variables. The drawer reads 19 public variables (--cffy-sidebar-bg-color, -radius, -padding, item colors and their active state, the overlay color and blur, …). See Sidebar → CSS variables.
  • Tabs CSS variables. UiTabs reads 14 public variables: the underline indicator, the border, tab text and its hover and active states, and --cffy-tabs-pills-* for pill tabs (container, radius, raised indicator). Pill tabs no longer repeat the base active color, so --cffy-tabs-link-active-color reaches them too. See Tabs → CSS variables.
  • Segmented control CSS variables. UiSegmentedControls reads 11 public variables for its frame, items and sliding pill. See Segmented controls → CSS variables.
  • Footer CSS variables. UiFooter reads 14 public variables for its colors, padding, the space above it (now independent of the padding), the title, links and icons. See Layout → Footer CSS variables.
  • Progress, spinner and skeleton CSS variables. UiProgressBar reads --cffy-progress-bg-color, -color, -label-color, -height, -radius, -font-size, -easing, -duration and -value (the fill, which the value prop sets and which replaces the private --_progress-width; see Migration); the spinner reads --cffy-progress-spinner-color and --cffy-progress-spinner-size, which UiProgressSpinner's size prop sets; skeletons read --cffy-skeleton-color and --cffy-skeleton-radius (the shimmer follows the color); the shapes loader reads --cffy-loading-shapes-color. See Progress and Skeleton.
  • Alert CSS variables. Alerts read 18 public variables: colors, radius, type, padding and gaps, the title, and --cffy-alert-snackbar-* / --cffy-alert-toast-* for snackbars. Alerts with an icon no longer repeat the base gap, so --cffy-alert-gap reaches them. See Alert → CSS variables.
  • Card CSS variables. Cards read 13 public variables for their surface, border, radius, padding, title and text, plus the selected border of selectable and link cards. The header and footer corners now follow the card radius instead of the compiled $card-border-radius. See Card → CSS variables.
  • Button CSS variables. Buttons read --cffy-btn-radius, -height, -padding-inline, -padding-block, -font-size, -font-weight, -icon-size, -shadow, -easing and -duration, shared by every variant; sizes, icon buttons, FABs and rounded keep their own values. See Button → CSS variables.
  • Chip CSS variables. Chips read --cffy-chip-bg-color, -color, -border-color and -radius; selected chips keep the theme's tonal colors. See Chip → CSS variables.
  • Button group, chip group, FAB group and toggle CSS variables.--cffy-btn-group-gap, --cffy-chip-group-gap, --cffy-fab-group-offset-block / -inline / -gap, and --cffy-toggle-btn-* for UiButtonToggleGroup (surface, border, radius, padding, subtitle and the hover and selected outlines). The selected toggle's inset ring now follows its border color. See Button group → CSS variables.
  • Badge CSS variables. Badges read --cffy-badge-bg-color, -color, -border-color, -radius, -font-size, -font-weight, -padding-block, -padding-inline and -icon-size, and badge groups --cffy-badge-group-gap. See Badge → CSS variables.
  • Accordion CSS variables. Accordions read 13 public variables for their surface, radius (the outer corners of a stacked group), padding, title and body text, icons, hover state, motion and the gap between items. See Accordion → CSS variables.
  • Avatar CSS variables. Avatars read 17 public variables for their radius, motion, initials tint and text, group overlap and ring, and the status dot's size, ring and colors. See Avatar → CSS variables.
  • List CSS variables. Lists read 26 public variables for the item surface, text, corners, padding, icon tile, image, active state, trailing arrow and motion. See List → CSS variables.
  • Popover CSS variables. Popovers read 15 public variables for the panel surface, border, radius, shadow, size limits, spacing, offset from the trigger, header text and motion. See Popover → CSS variables.
  • Tooltip CSS variables. Tooltips read --cffy-tooltip-bg-color, -color, -padding, -radius, -shadow, -font-size, -font-weight, -kbd-bg-color and -kbd-color. See Tooltip → CSS variables.
  • Breadcrumb CSS variables. Breadcrumbs read 12 public variables for the gap, link, hover and current-page colors, the current label's weight and maximum width, separators, text and icon size, and motion. Icon separators now follow --cffy-breadcrumb-separator-color. See Breadcrumb → CSS variables.
  • Stepper CSS variables. Steppers read 24 public variables for the indicator size, corners, text and pending, current and completed colors, the connector, labels, descriptions, the vertical gap and motion. See Stepper → CSS variables.
  • Timeline CSS variables. Timelines read 18 public variables for the gap, item spacing, connector, dot, icon and image markers, title, text and timestamp. See Timeline → CSS variables.
  • Table CSS variables. .table (and UiDatatable) reads 20 public variables for the surface, borders, cell padding, text, header, icons, selected and striped rows, the row hover motion, the sticky-header height and the pinned column. .td-text now follows --cffy-table-font-size. See Datatable → CSS variables.
  • Input CSS variables. Text inputs, textareas, selects and the color picker's field read --cffy-input-* variables for their size, padding, radius, surface, border, text, placeholder, shadow, hover, focus and invalid states, label and motion; --cffy-form-group-spacing sets the space below each field. Ranges read --cffy-input-range-* and the file dropbox --cffy-input-file-*. A range's thumb now stays centered on the track when either size changes. See Inputs → CSS variables.
  • Checkbox, switch and radio CSS variables. .form-check reads --cffy-input-check-* variables for the box size, gap, spacing, surface, border, checked color, checkbox radius and motion, and switches --cffy-input-switch-* for the track and thumb. See Inputs → Checkbox, switch and radio CSS variables.
  • Prefix and suffix CSS variables. Input adornments follow the field's --cffy-input-radius, --cffy-input-border-* and --cffy-input-bg-color, and read --cffy-input-group-bg-color, -color, -padding-inline, -font-size and -icon-size. See Inputs → Prefix and suffix CSS variables.
  • OTP CSS variables. .form-otp reads --cffy-input-otp-gap, --cffy-input-otp-font-size and --cffy-input-otp-font-weight; the boxes follow the other --cffy-input-* variables. See Inputs → OTP CSS variables.
  • Divider CSS variables. Dividers read --cffy-divider-thickness, -color, -spacing, -inset, -text-gap, -text-color and -text-font-size. See Divider → CSS variables.
  • Carousel CSS variables. The .carousel scroll buttons (native ::scroll-button() and the .carousel-btn fallback) read --cffy-carousel-btn-size (default 44px), --cffy-carousel-btn-bg-color (--cffy-surface-body-pane), --cffy-carousel-btn-color (--cffy-on-background), --cffy-carousel-btn-shadow (--cffy-shadow-sm) and --cffy-carousel-btn-radius (--cffy-radius-full).
  • Header, hero and subheading CSS variables. UiHeaderContent reads --cffy-header-* (spacing, gap, title and description), UiHeroContent--cffy-hero-* (gaps, spacing, line length, description and the actions' offset and gap) and UiSubheadingContent --cffy-subheading-gap and --cffy-subheading-actions-gap. See Layout → Header CSS variables.
  • Navigation bar CSS variables. The bottom navigation bar reads --cffy-navigation-bar-bg-color, -shadow, -item-color, -item-active-color, -item-hover-color, -font-size, -icon-size, -indicator-color, -indicator-radius, -easing and -duration. See Navigation bar → CSS variables.
  • Link, code and kbd CSS variables. Base links read --cffy-link-color, -hover-color, -active-color, -underline-color, -easing and -duration; <code> reads --cffy-code-color and --cffy-code-font-size; and <kbd> reads --cffy-kbd-bg-color, -color, -font-size, -radius and -shadow. See Typography → CSS variables.
  • Icon wrap CSS variables. .icon-wrap reads --cffy-icon-wrap-padding (default 20px), --cffy-icon-wrap-radius (--cffy-radius-full), --cffy-icon-wrap-bg-color (--cffy-background), --cffy-icon-wrap-color (--cffy-primary-a10) and --cffy-icon-wrap-tint-alpha (10%, the strength of the colored variants' tint). Sizes keep their own padding and corners, and colored variants their own colors.
  • Icon-only tabs. UiTabs takes iconOnly: tabs with an icon render as square icon buttons (.tabs-icon-only), with the label kept as visually hidden text for screen readers and a native title tooltip. See Tabs → Icon-only tabs.
  • State layer tokens. --cffy-state-layer-color (default --cffy-primary-base), --cffy-state-hover-opacity (8%) and --cffy-state-pressed-opacity (12%) set the hover and pressed background of every interactive surface in one place. See Customization → Hover and pressed states.
  • Container tokens for tinted surfaces. For primary, secondary, accent, success, warning, danger, info and muted, --cffy-<color>-container is the tinted surface (a pale tint in light mode, a dark one in dark mode) and --cffy-on-<color>-container the text and icon color on it, so a custom tonal block takes two tokens. Tonal buttons, badges and alerts paint the pair; the .text-*-emphasis utilities and the active navbar, popover-menu and list states read the text token. --cffy-on-primary-container existed before as plain black or white and was unused; it now carries the primary tint. The .bg-<color>-container and .text-on-<color>-container utilities apply them. See Tonal tokens.
  • UiButtonMenuItem keepOpen. Items now close their menu when clicked (see Fixed); set keep-open on an item that toggles a setting or opens a further step so the menu stays put.
  • UiInputPassword, UiInputSearch and UiInputTags. A password field with a show/hide toggle (v-model:revealed), a search field with a clear button that emits search on Enter and clear on clear or Esc, and a tags field that keeps a string[] (Enter or a separator adds, Backspace removes, pasted lines split, max caps the count, and a duplicate or a tag past max is announced and emitted as reject). They take the shared input props. See Inputs.
  • Inline input adornments. UiInputText takes adornments="inline", which puts the #prefix and #suffix slots inside the field (.input-group-inline) for icons and icon buttons, and autocomplete, which reaches the field instead of the wrapper. The tags field is .form-tags, which reads the --cffy-input-* variables plus --cffy-input-tags-gap.
  • Inverse color tokens. --cffy-<color>-inverse (primary, secondary, accent, success, warning, danger, info and muted) is the color to use on an inverse surface such as a toast, snackbar or tooltip, which is dark in light mode and light in dark mode. It takes the light brand tone in light mode and the dark one in dark mode, kept light or dark enough to read on any brand color; browsers without relative color syntax use the plain brand tones. The .text-<color>-inverse utilities apply them, and the inverse-color() SCSS function builds one. See Tonal tokens.
  • Container width scale. --cffy-container-3xs (16rem) to --cffy-container-7xl (80rem) give component widths one t-shirt scale, and max-w-{size} utilities read the same steps. The sidebar, popovers, popover menu, toasts and dialogs take their widths from it, so one token resizes everything on that step. See Customization → Container widths.
  • Utility columns in UiDatatable. Columns take hideLabel, which keeps the label for screen readers but hides it in the header, hideable: false, which keeps the column on screen and out of the column manager, and fit, which sizes it to its content through the new .table-col-fit class. A column with an empty label is no longer listed in the column manager as a blank row, and an always-visible column no longer counts toward the one column the manager keeps on screen. The column-manager and column-toggle slots receive only the hideable columns. See Datatable.
  • UiPagination. Page buttons in a <nav>, bound with v-model:page, sized from total and pageSize or from totalPages. Long ranges collapse around the current page (siblingCount) with a fixed number of buttons, and below 600px, or with compact, the numbers give way to Page 3 of 12. showEdges adds first and last buttons, labels translates the text, and page moves are announced. The .pagination classes read --cffy-pagination-gap, -radius, -current-bg-color, -current-color and -status-color. See Pagination.
  • Datatable pagination. UiDatatable takes pagination: { pageSize } and v-model:page: it sorts every row, shows one page and renders a UiPagination under the table. Select-all covers the current page, and the page goes back to 1 when the sort, the page size or the row count changes. See Datatable.
  • UiInputCombobox. A field that picks one value from a list you can search, a native replacement for a filterable select. It takes options, optionLabel and optionValue like UiInputSelect, plus optionGroup for headings, optionDisabled, clearable and an #option slot. Typing filters by label, ignoring case and accents; :filterable="false" makes it select-only with type-to-jump. The keyboard follows the WAI-ARIA combobox pattern, with focus kept in the field. The list is a popover placed with CSS anchor positioning, so it opens over dialogs and outside clipping containers and flips above the field when needed; browsers without anchor positioning get the same placement from a small script. It's styled by the new .listbox-popup / .listbox-option classes and --cffy-listbox-* variables, and .input-group-inline now fits a second suffix. With remote the server filters: typing emits search after a pause (searchDelay, minSearchLength), loading shows a searching row, results are announced, and a picked option keeps its label across searches. freeText keeps typed text that isn't an option. See Inputs → Combobox.
  • UiInputMultiSelect. The same searchable list for several values, a native replacement for a multi-select. The model is an array in pick order; the field shows a removable chip per value like UiInputTags (Backspace removes the last), and past maxChips values a maxChipsLabel summary such as "5 selected" instead, so the field keeps to one row. The list stays open while you pick, max disables the other options once it's reached, and it emits add and remove. It takes the same remote search, and freeText adds typed text through an "Add “…”" row (.listbox-option-create, --cffy-listbox-create-color). See Inputs → Multi-select.
  • UiCalendar. An inline month grid that picks one date, several dates or a range (mode), with Date values at midnight local time. It shows one or more months side by side (months), honors min, max and a disabledDates callback, and takes month and weekday names and the first day of the week from the locale through Intl. A pending range follows the pointer, v-model:month controls the visible month, fluid fills the container's width like a mobile app calendar, and the #day slot adds content such as dots. Keyboard navigation follows the WAI-ARIA date grid (arrows, Home/End, Page Up/Down, Shift for years). The .calendar classes read 15 --cffy-calendar-* variables, and days take the control shape. See Calendar.
  • UiInputDate. A date field with a calendar popup, a native replacement for PrimeVue's DatePicker. It holds a Date, { start, end } with mode="range" (written once both ends are picked) or a Date[] with mode="multiple", and accepts dates typed in the locale's numeric order, several separated by commas. presets lists shortcuts built with the new datePresets helpers (lastDays(7), lastMonths(6), lastYear(), …), time adds hour and minute fields (or seconds, with minuteStep) with a Now button, timeOptions lists time slots beside the calendar instead (disabledTimes marks booked ones), confirm holds the picks until Apply (on by default with times, or for a range with presets), and trigger="button" shows the matching preset ("Last 7 days") or the dates on a button, with From and To fields in the popup. The popup is anchored like the combobox list, Esc closes it without closing a dialog around it, and below 600px it becomes a bottom sheet with preset chips and stacked months. The .calendar-popup classes read 10 --cffy-calendar-popup-* and --cffy-calendar-preset-* variables. See Inputs → Date.
  • Shape roles. Components take their corners from three tokens: --cffy-shape-container (cards, dialogs, popovers, menus, alerts, lists), --cffy-shape-field (inputs, selects, the file drop zone) and --cffy-shape-control (buttons, chips, toggle buttons, sidebar items). Set the three to make the whole interface sharp, soft or pill-shaped at runtime; nested and tall shapes derive from them, and badges and avatars stay round. $shape-container, $shape-field and $shape-control set the defaults in SCSS. See Customization → Shape roles.
  • Localization. app.use(ColorffyUI, { locale, labels }), the Nuxt module's colorffyUI: { locale, labels: 'es' } option or UiConfigProvider set the locale and every text the components write on their own: button names, the "Optional" hint, empty states, screen-reader announcements and date names. English and Spanish ship as @colorffy/ui/locales/en and @colorffy/ui/locales/es, useColorffyConfig() switches language at runtime, and props such as clearLabel still override one component. Texts with placeholders can also be functions of their values (LabelTemplate), for singular and plural wording such as the Spanish pack's "1 seleccionado". See Localization.
  • Vue transitions. Pass fade, slide-up, slide-down, slide-start, slide-end, scale or list as the name of a <Transition> or <TransitionGroup>; list also moves the items that stay. They share six variables for duration, easing, distance and scale (--cffy-transition-enter-duration, --cffy-transition-distance, …), and reduced motion keeps the fade without the movement. See Effects → Transitions.
  • Keyboard navigation in menus. UiButtonMenu follows the WAI-ARIA menu button pattern: ↓ or ↑ on the button opens the menu on its first or last item, ↑ and ↓ move between items and wrap, Home and End jump to the ends, and a letter moves to the next item starting with it. → opens a UiButtonMenuSubmenu on its first item and ← closes it, Tab closes the menu and moves on, and an item picked from the keyboard gives focus back to the button. The panel is a role="menu" of menu items and separators. UiPopoverMenu takes the same arrow, Home, End and letter keys. See Button Menu → Keyboard.
  • UiButtonMenuItem checked. Makes the item a menu checkbox (role="menuitemcheckbox") and sets its state. The datatable's column manager uses it, so screen readers announce which columns are shown.
  • autofocus on a field in a modal. UiModal focuses the control of a field component marked autofocus (<UiInputText autofocus>) when it opens. The attribute lands on the component's wrapper, and browsers only autofocus an element that takes focus itself, so the modal opened on its first button instead. See Modal.

Changed

  • @colorffy/css doesn't install floating-vue. It styles FloatingVue's poppers but doesn't need the package, so floating-vue is an optional peer and CSS-only projects no longer get it; @colorffy/ui still requires it. @colorffy/ui also declares @nuxt/kit, which its Nuxt module imports, as an optional peer.
  • Mask shapes use the standard mask-image only. Each shape declared its SVG twice, once for -webkit-mask-image. The unprefixed property has worked in every major browser since December 2023 (Safari 15.4), so the prefixed copies are gone, which halves the masks' minified size; Safari before 15.4 shows the element without the shape.
  • The sidebar's active-icon color is a public variable. The leading icon of the current-route item read --_drawer-active-color, a private variable nothing declared. Set --cffy-sidebar-item-active-icon-color instead; see Migration.
  • Small ranges scale their track and thumb. size="sm" on UiInputRange (.form-range.form-sm) used to shrink only the field's height, leaving the full-size track and thumb. It now also uses a 0.5rem track and a 1.35rem × 0.25rem thumb.
  • One hover and pressed background for every interactive surface. Each component used to pick its own surface token for hover (the card the overlay surface, list items and dropdown items container-high, tabs and the sidebar container, table rows surface-a10, popover menu items a primary tint), so the same gesture looked different from one component to the next and barely showed on some surfaces. List items, selectable and link cards, accordion headers, dropdown and popover-menu items, tabs, chips, table rows, the file dropbox, and sidebar, navbar and menu-tools links now mix their own background with the primary color, the same tint the surface ramp is built from: 8% on hover, 12% while pressed or focused from the keyboard. Navbar, sidebar, popover-menu and menu-tools links gain that pressed state, accordions highlight only while the header itself is hovered, disabled list items no longer react to hover, and the destructive popover-menu item and a file dropbox holding a valid file use a danger or success layer.
  • Tonal ramps mix in oklab. The a10–a90 ramps, the surface ramp, the disabled tokens, tonal button, badge and alert backgrounds, and the remaining component mixes blended in srgb. They now mix in oklab, like the state layer and text selection, with the percentages retuned so each token keeps its lightness: nothing moves more than about 0.03 ΔE in OKLab, and the deepest light-mode steps come out a touch darker. Light tonal backgrounds are slightly less saturated, since oklab doesn't add the extra chroma srgb picks up when mixing toward white. In dark mode --cffy-tonal-dark-intensity now defaults to 58% and --cffy-tonal-light-intensity to 8% (were 70% and 15%). Overridden intensities blend in oklab too, so a custom dark-mode value comes out darker than before; compare it against the new defaults.
  • Dark-mode tonal ramps darken step by step. In dark mode the primary, secondary and accent ramps darkened from a10 to a80 and then jumped back to a light tint at a90, and the success, warning, danger, info and muted ramps mixed toward white from an already light base, so they stayed near-white with a90 landing between a60 and a70. Every ramp now darkens from one step to the next in both modes, a90 included; the light text tone it used to hold is --cffy-on-<color>-container, which the library now reads wherever it read a90. Filled semantic buttons darken on hover and press instead of lightening, tonal backgrounds and bordered-badge borders come out a step deeper, and --cffy-muted-a10 (secondary text) is slightly dimmer. If your own styles read a90 as a text color, see Migration.
  • Text on a brand color follows the brand color. --cffy-on-<color> was fixed per color (white on primary, secondary, success, danger and info in light mode, black in dark mode, always black on accent and warning), so a light custom color kept white text unless you overrode it. Each one now picks black or white from the lightness of the solid fill the components paint, --cffy-<color>-a10, whichever has more contrast, in both modes. In the default theme, info fills switch to black text (white measured 3.1:1, black measures 6.8:1). Values you set yourself still win. Browsers that can't evaluate the calculation keep the fixed values: those without relative color syntax, and Safari 16.4–17, which implements an older draft of it.
  • Tonal buttons step from their container. Hover and pressed used to lower --cffy-tonal-dark-intensity by 10% and 20%, so a tonal button ignored any surface you set. Each state now mixes the container 15% and 30% back toward the fill, which matches the previous colors within ΔE 0.03 and carries an overridden --cffy-<color>-container through every state. For SCSS users, btn-tonal() and container-tonal() now take the surface instead of computing it; see Migration.
  • Events drop the on prefix. UiButton, UiButtonMenu and UiButtonTooltip emit click (with the MouseEvent) instead of onClick; the inputs emit update instead of onUpdate; UiModal and UiConfirmModal emit close instead of onCloseDialog; UiButtonToggleGroup emits optionClick instead of onOptionClick. Vue adds on itself when it builds a listener, so the old names had to be written @on-click, while a plain @click only worked because the native click fell through. A disabled or loading button never emits click. See Migration.
  • Sidebar width hook renamed. --cffy-nav-drawer-width is now --cffy-sidebar-width, named like every other component hook; the default is still 18rem. See Migration.
  • Dialogs and toasts snap to the container scale. Their widths moved to the nearest step: the confirm dialog tops out at 24rem (was 25rem), sm at 28rem (was 26rem), md at 42rem (was 40rem) and lg at 56rem (was 60rem), with minimums following the step below; the toast minimum is 18rem (was 300px). The sidebar, side sheet, popover and popover menu keep their sizes.
  • UiDatatable's toolbar buttons use the default icon-button shape. The column toggle and column manager were small-radius squares (shape-sm); they now use the same shape as any other icon button in the toolbar.
  • Tab and step selection works with v-model. UiTabs and UiSegmentedControls emit update:activeTab instead of updateActiveTab, and UiStepper emits update:activeStep instead of updateActiveStep, so v-model:active-tab and v-model:active-step keep the selection in sync. See Migration.
  • id renders as given. Buttons, cards, accordions and link tooltips prefixed it (button-save), and inputs suffixed it on the field (email-input-text), so the value you passed could not be used for anchors, <label for>, scripts or tests. The id now lands unchanged on the main element. See Migration.
  • Spacing utilities follow the spacing tokens. The margin, padding, flow, block-spacing, position (top-* …), translate and gap utilities compiled fixed rem values, so changing --cffy-space-unit rescaled components but left utilities behind. They now read --cffy-space-* and --cffy-space-unit: same class names and the same values at the default unit, no markup changes. px stays a 1px hairline.
  • Empty-state and loading titles use the paired line-height.UiEmpty, UiLoading, UiShapeLoading and UiExpressiveLoading titles moved from the legacy font-size classes to the t-shirt ones, which pair each size with its line-height, so a 20px title's line box grows from 20–24px to 28px. Icons keep their tight line box.
  • UiSidebar's header, body and footer slots render their regions. They used to place content inside .drawer-content without a wrapper, so they only worked when filled with the region components. Like UiCard and UiModal, each slot now renders inside its own .drawer-header / .drawer-body / .drawer-footer, skipped when empty, with headerClass / bodyClass / footerClass for extra classes.
  • Private variables are named after their hooks. Every --_* variable fed by a public hook now carries that hook's name (--_card-gutter-x → --_card-padding-inline, --_nav-tabs-link-color → --_tabs-link-color, --_drawer-* → --_sidebar-*, --_form-* → --_input-*), and the --_color-pickerp-width typo is fixed. Rendering is unchanged. Private variables were never public API, but if your CSS overrode one, set its public hook instead; see Migration. UiDatatable's stickyHeight now sets the public --cffy-table-sticky-max-height.
  • Every public custom property is namespaced with --cffy-. Unprefixed names like --space-* and --theme-* could collide with other libraries on the same page. The semantic --theme-* tokens now use the namespace in place of theme- (--theme-primary-a10 → --cffy-primary-a10), and every other public token, component variable, animated @property and internal anchor name gains it (--space-16 → --cffy-space-16, --card-bg-color → --cffy-card-bg-color). Private --_* variables are unchanged, and the unused $property-prefix SCSS variable is gone. See the migration guide for the find-and-replace.
  • @colorffy/ui declares @colorffy/css as an optional peer dependency. The components set --cffy-* variables that only @colorffy/css 3.x reads, so pairing UI 3.0 with CSS 2.x left them without effect and without a warning. @colorffy/ui now lists @colorffy/css ^3.0.0 as an optional peer, so package managers warn about a mismatched version and still install UI on its own.
  • Groups of text buttons sit closer. A .btn-group whose buttons are all .btn-text uses a --cffy-space-4 gap instead of the 12px / 8px one. A text button has no visible edge, so its padding already spaces it and the wider gap pushed the icons apart. Text-button toolbars, including the datatable's when its slots and toolbarButton use the text variant, now match the spacing of a hand-built icon row. Groups that mix in any other variant keep the wider gap.
  • The datatable's column manager stays open while you toggle columns. Each pick used to close the menu, so hiding three columns took three trips. Its items now keep the menu open (keepOpen); click outside or press Esc to close it.
  • Selected table rows are tinted. --cffy-table-selected-bg-color defaulted to the page background, so a selected row looked almost the same as the others in light mode. It now defaults to --cffy-primary-container.
  • Radius settings reach the related parts. With the shape roles, a $form-border-radius you set also shapes the file drop zone, input-group adornments and checkboxes (half the field radius), and $button-border-radius shapes the floating action button (twice the button radius). They used fixed radii before. The defaults look the same.
  • Menu buttons have no default tooltip. UiButtonMenu's tooltipText defaulted to 'menu', so every menu button without one showed a lowercase English "menu" tooltip, text buttons included. The tooltip now only shows when you pass tooltip-text, and an icon-only trigger without a title or tooltip is named by the new buttonMenu.ariaLabel label ("Menu", "Menú"). See the migration guide.
  • Every part follows the shape roles. A few parts kept a fixed step of the radius scale, so a sharp or pill theme left them behind. Navbar links, tab links, <kbd>, breadcrumb links and the navbar initials now take --cffy-shape-control; tables, <details>, list and timeline icons and images, icon tiles and the headless dialog icon follow --cffy-shape-container, never rounder than before; skeletons follow the field role; and the mobile dropdown sheet and the navigation drawer round at twice the container radius, like bottom sheets. Icon buttons, carousel arrows, the alert close and chip remove buttons, progress bars, the navigation bar indicator and the ends of connected button groups stay round unless --cffy-shape-control is 0, and the pill tabs track no longer keeps rounded corners around square pills. With the default roles everything looks the same; the dropdown sheet and drawer round at 24px instead of 25px.
  • The shape loader's angle is --cffy-shape-loading-angle. It was --cffy-shape-angle, which now reads like a shape role.
  • Toggle buttons are named by their content. UiButtonToggleGroup named each option "Pro (selected)", repeating the state its radio role already announces, and its badge "Tier: Save 20%". Options now take their name from their title, text and badge.
  • The dark-mode danger color is a clear red. --cffy-color-brand-danger-50, the danger base in dark mode, was oklch(90% 0.04 12), a pale pink that error text and borders barely set apart from white. It's now oklch(80% 0.11 20), about 7:1 against dialogs and 9:1 against the page.
  • Active navbar links keep their size. The current-page link widened its inline padding to --cffy-space-16 and turned extra bold, which pushed the links beside it. It now only changes its background and color.

Removed

  • --cffy-on-primary-inverse, --cffy-on-secondary-inverse and --cffy-on-accent-inverse. They held the same value as --cffy-on-background and nothing in the library read them. See Migration.
  • Legacy font-size tokens and classes. The ordinal --fs-100 … --fs-600, --fs-sm-100 … --fs-sm-500 and --fs-xl-100 tokens and their .fs-* classes, deprecated since 2.5, are gone. Use the t-shirt scale (--cffy-fs-4xs … --cffy-fs-5xl, .fs-base, …); the t-shirt classes also set the paired line-height. See the rename table.
  • Deprecated SCSS variables. $space-1 / $space-2 / $space-3 (use var(--cffy-space-16/32/48)) and the color and font variables deprecated in 2.8 ($primary, $secondary, $accent, $success, $warning, $danger, $info, $muted, $primary-colors, $font-*) are gone. Nothing read them; a @forward … with (…) that still sets one now fails to compile, so drop it and set the --cffy-color-brand-* / --cffy-font-* tokens instead. See Migration.
  • ISegmentedTab.position. Ignored since the segmented pill started reading the active tab's real geometry; the field is gone and ISegmentedTab is now an alias of ITabItem. Delete position from your items.
  • UiPopoverMenu's identity props and body-extra slot. user, avatarUrl, avatarCustomClass and subtitle rendered nothing since the header became a title plus the close button; put the identity in the header slot with UiPopoverMenuUser. body-extra is gone too: to add rows to a menuItems menu, render the body yourself in #body. See the migration guide.
  • UiInputFile's large prop. Use size="lg", which every input shares, for the taller dropbox.
  • BaseSkeleton's isThumbnail prop. Use variant="thumbnail".
  • UiEmpty's #button slot. The legacy fallback inside #action is gone; rename the slot to #action.
  • UiSidebarHeader, UiSidebarBody and UiSidebarFooter, and the default slots of UiSidebar and UiPopoverMenu. Panel components now take their regions through named slots only. Move sidebar regions into #header, #body and #footer, and a menu's default-slot content into #body. See the migration guide.
  • UiButtonToggleGroup's groupLabel prop. It was declared but never rendered; ariaLabel names the group. See the migration guide.
  • UiButton's tooltipText and placement props. They came from the props every button shares, but UiButton never showed a tooltip. Use UiButtonTooltip; it, UiButtonMenu and UiLinkTooltip keep both. See the migration guide.
  • UiPopoverMenu's auth prop. It was declared but never read. See the migration guide.
  • UiModal's isHeadless prop. It was declared but never read; mode="headless" gives the headless layout. See the migration guide.
  • UiDatatable's skeletonCols, skeletonColExpanded and isExpanded props. The loading skeleton always drew one cell per visible column and ignored skeletonCols, while isExpanded swapped in skeletonColExpanded cells that no longer matched the header. The skeleton now always matches the visible columns; skeletonRows still sets the row count. UiTableSkeleton keeps these props for standalone use. See the migration guide.
  • The .menu-tools mega menu. .menu-tools-container and its tabs, tool grid and call-to-action cards served one app's navigation, and no component rendered them. They are gone, along with .navbar:has(.menu-tools-visible). See the migration guide.
  • .pwa-alert-container. The PWA install/offline prompt (.pwa-alert-container, .pwa-content, .pwa-content-text-wrapper, .pwa-actions) served one app and no component rendered it. Build the prompt from UiAlert with type="snackbar" and actions. See the migration guide.
  • .segmented-control and .ripple. The legacy two-segment .segmented-control (UiSegmentedControls uses .tab-segmented-control) and .ripple, .ripple-dark, .ripple-primary and .ripple-secondary are gone, along with _button-effects.scss. The ripple-color() mixin stays. See the migration guide.
  • The PrimeVue skin. scss/components/_prime.scss, which styled PrimeVue's MultiSelect, Select, ColorPicker, Toast, ConfirmDialog and InputSwitch and set its --p-* tokens, is gone. UiInputCombobox, UiInputMultiSelect and UiInputDate replace Select, MultiSelect, AutoComplete and DatePicker; see the PrimeVue migration.
  • Unused helpers. Nothing in Colorffy UI used these:
    • .hover-rotate, .hover-scale and .vr.
    • .placeholder-list and .placeholder-loader, whose placeholderLoading animation never existed.
    • The pulse, glitch, shake-x, shake-y, fade-in-left, slide-in-down, spin-wheel, animate-wheel, shine-light, dots-scroll and animate-gradient-shape-* keyframes.
    • The slide, slide-inline, fade-inline, table and table-td-fade transitions, replaced by the new transitions (slide-block stays).
    • The spinning icon on .nav-link hover.
    • The custom color-picker popover styles (.color-picker-popper, .color-swatch, .form-color-picker-wrapper). UiInputColorPicker uses the native color input.
    • layout/_grid.scss, a copy of layout/_flex-container.scss; import @colorffy/css/scss/layout/flex-container instead.

    See the migration guide.
  • Unused tokens and SCSS API. --cffy-img-testimonial-filter and -invert, --cffy-vfw-400 to -800 and --cffy-surface-text-field; the @property registrations of --cffy-color-wheel-radius, --cffy-element-radius, --cffy-dots-scroll-position, --cffy-gradient-shape-* and --cffy-noise-lines; the $dark, $scheme-colors and $fs-table variables; and generate-static-gradients(), which matched generate-gradients() (its unused third argument is gone too).

Fixed

  • Menus and tooltips work inside modals. UiButtonMenu and every tooltip (UiTooltip, UiButtonTooltip, UiLinkTooltip, the sidebar dropdown and link tooltips) rendered their popup at the end of <body>. UiModal and UiConfirmModal open with showModal(), which puts the dialog on top of the page and makes everything outside it inert, so those popups sat behind the backdrop and ignored clicks: a UiDatatable column manager inside a modal couldn't be opened. Popups now render inside the nearest <dialog> or [popover] with fixed positioning, .dialog and .popover let them overflow their box (their body still scrolls), and clicking a menu item no longer counts as a click outside the modal. Colorffy CSS also gives FloatingVue poppers their top: 0; left: 0 origin, which only FloatingVue's own stylesheet set before, so they're placed correctly without it.
  • Esc closes a menu without closing the modal around it. The menu didn't handle Esc, so the key reached the <dialog>, which closed. UiButtonMenu now takes Esc while it's open, closes, and returns focus to its button. Its button also reports aria-expanded and aria-controls.
  • UiButtonTooltip no longer announces itself as collapsed. Its optional ariaExpanded prop is a boolean, which Vue reads as false when it isn't passed, so every tooltip button said aria-expanded="false". It's now only set when you pass it.
  • Reopened modals start at the top. UiModal and UiConfirmModal kept the body's scroll position from the last time they were open, so the first fields could be scrolled out of view.
  • A modal's close button stays on the title row. .dialog-header wrapped its children, so on a phone a title and subtitle wider than the space left pushed the close button onto a line of its own. The header no longer wraps: the title block shrinks and its text wraps, with a --cffy-space-12 gap before the button.
  • Select text stops before the arrow. .form-select had the same inline padding on both sides, so in a narrow field a long option ran under the arrow icon. The end padding now clears the icon at every size: 2.25rem, or 1.75rem with .form-sm and 2.75rem with .form-lg.
  • Datatable captions stay readable on phones. The <caption> sat inside the table's horizontal scroll, centered across the whole table, so on a narrow screen part of it was cut off. UiDatatable keeps the caption for screen readers and shows its text below the scroll area as .table-caption, in the muted text color.
  • The navigation bar's indicator leaves when no item is active. It started on the first item and only moved to a link with the router's exact-active class, so on a page outside the bar (/billing) it stayed on Home, and with plain links and activeItem it never moved. It now also follows aria-current="page", which activeItem sets, and fades out when no item is active.
  • Tables hide their scrollbar in every browser. .table-responsive hid its scrollbar only through ::-webkit-scrollbar, which Chrome ignores once scrollbar-width is set, as the reboot does for every element, so a thin bar showed under tables that scroll sideways. It now also sets scrollbar-width: none.
  • Selects in tables keep their width. A .form-select fills its cell (width: 100%), which gives an automatic table nothing to size the column by, so on a phone a select in a table shrank to a square. Inside .table a select is now at least as wide as its longest option.
  • --cffy-radius-full makes pills and circles at any size. It was 50px, so a 150px avatar-lg was a rounded square and a pill taller than 100px lost its round ends. It is now 9999px. The raw .border-radius-50 utility keeps 50px.
  • Small dialogs fit their content on phones. Below 992px every dialog became a bottom sheet 75% of the screen tall, including size="sm", whose content keeps its own height. The form sat at the top of a mostly empty sheet, and a tap on the empty part landed outside the content, which closed the dialog. Small dialogs now grow with their content, up to 75% of the screen.
  • Tables don't widen the page on phones. The screen-reader label of a hideLabel column is positioned absolutely, and .table-responsive wasn't a positioned box, so the label escaped the table's horizontal scroll and widened the page. .table-responsive is now position: relative. It also hides vertical overflow (except with a sticky header), which removes a scrollbar that a 1px caption rounding left next to some tables.
  • The indeterminate checkbox shows a dash. There was no :indeterminate style, so the datatable's header checkbox looked unchecked while some rows were selected. It now shows a dash on the checked color.
  • Pasting a list into UiInputTags keeps every value. Only pastes with line breaks were split; a comma-separated line went into the field, where maxlength cut it short before it was split. Pastes that contain the separator are now split too.
  • Nuxt pages fade in, not only out. The page and layout transitions styled Vue 2's -enter class, which Vue 3 renamed -enter-from, so with pageTransition: { name: 'page' } the old page faded out and the new one appeared at once. They now fade both ways.
  • Esc no longer clears a disabled or read-only UiInputSearch.
  • List items animate their background and border. The item transition had no duration, so hover, pressed and active changes snapped. They now ease over --cffy-duration-200.
  • UiExpressiveLoading title sizes were inverted. size="sm" used the legacy fs-500 (20px), the default fs-600 (16px), and lg a fs-700 class that never existed, so it fell back to 15px: the larger the size, the smaller the title. They are now 16px, 20px and 24px.
  • UiGridSkeleton previews have their height again. Each card set --fixed-size: 6.25rem, but the .h-fixed utility reads --cffy-h-fixed, so the preview block fell back to its 2.5rem minimum. It now sets --cffy-h-fixed.
  • Popover menu items no longer restyle what you put in them. The item colored and resized every <p> and <i> inside it, so buttons placed in the trailing slot (a theme switcher, say) lost their own text and icon colors. The rules now apply only to the item's label, leading icon and trailing icon.
  • UiButtonMenuItem closes its menu. Clicking an item left the dropdown open until you clicked elsewhere, and items inside a submenu left both levels open. A click now closes the whole menu.
  • UiModal renders title and message. Both props were declared and documented but never shown. They now fill the header and body when those slots are empty. Clicks outside a closed modal also stopped emitting a close event: the outside-click listener ran whether or not the dialog was open.
  • UiConfirmModal shows loadingLabel and emits close. The confirm button kept its label while loading, and the documented close event was never emitted. Both now work, and outside clicks are ignored while it is closed.
  • UiCard no longer shows its title as a browser tooltip. The title prop was also bound to the root's native title attribute. The card body also stopped re-applying the card color, so a text utility on the card (text-on-primary) now reaches the body.
  • UiAvatar shows the photo when both src and initials are set. Initials took priority, so the image never appeared. They are now the fallback for a missing or broken image, as documented.
  • UiButtonTooltip links. to, href, as and type were accepted but not passed to the button, so a tooltip button couldn't navigate.
  • UiSegmentedControls renders tab icons and badges. Its tabs accept icon and badge like UiTabs, but only the label was drawn.
  • UiBadge renders a <span>. The <div> root made badges invalid inside paragraphs and buttons (tabs, menu items), which broke SSR hydration when a badge sat in a <p>.
  • UiTimeline styles the neutral variant. It was accepted but had no marker colors; it now uses the muted container pair.
  • UiLinkTooltip honors placement. The prop was declared but never passed to the tooltip.
  • UiInputSelect placeholder is in English. The default was "Clic para seleccionar"; it is now "Select an option".
  • UiNavbarNav position aligns as documented. From 992px, start pushed its group away from the brand (margin-left: auto) and end pushed away from the far edge (margin-right: auto), so a start and an end group floated toward the middle with a gap after the last item. start now sits next to the brand and end against the far edge. If you used two start groups to push everything to the right, make the right-hand group end.
  • Prefixed and suffixed inputs line up. An input with a prefix or suffix nests an .input-group inside its .form-group, and both added the bottom margin, so the field took twice the spacing. Inside a navbar item the form margin is now dropped entirely, and the prefix and suffix follow variant="transparent" (no border or background) and rounded (full radius) instead of keeping the default boxed look.
  • .grid-main-content no longer scrolls sideways. Its content column was 1fr, which cannot shrink below the widest thing inside it, so a wide table pushed the whole page past the viewport instead of scrolling in its own .table-responsive. The column is now minmax(0, 1fr).
  • Alert actions stay beside the text. An alert with a title switched to a single column, which pushed its actions onto a row of their own at the bottom. Actions now sit next to the content and only drop below it when the alert is 450px wide or less.
  • The segmented control indicator moves without lagging. The labels transitioned font-weight, so with a proportional font the active label kept growing while the indicator travelled toward it, and the indicator trailed behind a moving target. The weight now changes at once.
  • UiEmpty centers its action. Buttons in the action slot sat at the left edge because they are block-level; the slot is now wrapped in a centered UiButtonGroup, which also spaces several actions.
  • List item actions keep their width. The item content took width: 100%, so the list-action slot was squeezed until its button labels wrapped. The content now takes the free space and the actions keep their natural width.
  • The sidebar rail centers its header. The rail nudged an image or icon logo with a fixed 1rem offset and left anything else at the start edge. The header now centers whatever logo it holds.
  • UiShapeLoading subtitle is readable. It carried text-muted, which overrode the label color meant for text on the blob, so the subtitle faded into the shape. It now uses the same color as the title, and both lines are centered on the shape (a shorter subtitle used to sit at the left edge).
  • <kbd> keeps its light-mode background in older browsers.--cffy-surface-kbd called color-mix() without an interpolation space, which only browsers supporting the newer optional-space syntax accept, so elsewhere the token was invalid and the key rendered unpainted. It now names oklab, the space those browsers default to, so they render it the same.
  • UiChip renders its id. The prop was declared but bound to neither root, so the chip had no id. It now lands unchanged on the chip's root. UiChipGroup no longer passes each option's id down as a DOM id, so option ids such as list don't collide with ids elsewhere on the page.
  • Dialogs emit close when closed with Esc. UiModal and UiConfirmModal emitted close from closeDialog() only, so pressing Esc closed the native <dialog> without telling the parent. Both now emit close from the dialog's own close event, which covers closeDialog(), outside clicks and Esc alike. The event now fires once the dialog has closed, a moment after closeDialog() returns.
  • UiConfirmModal honors closeOnClickOutside. It closed on every outside click whatever the prop said. It now defaults to true, as on UiModal, and false keeps the dialog open.
  • A disabled UiAccordion stays put. disabled only set aria-disabled on the <details>, so the header still toggled the item and nothing looked disabled. The header now ignores clicks and Enter/Space, leaves the tab order, carries aria-disabled, and renders at reduced opacity with a not-allowed cursor and no hover layer.
  • UiIconMaterial applies size and color. Both props were declared but never bound, so the icon always took the surrounding text size and color. size now sets the glyph size (xs … xl = 20 … 48px, numbers are px) and color its color; left unset, the icon follows the text as before.
  • UiLinkTooltip applies fluid. The prop was declared but never applied; it now makes the link full width, like UiButton.
  • Using the SCSS mixins adds nothing to your CSS. abstracts/mixins declared eight @property rules and wrote its doc comments as CSS comments, so every stylesheet or Vue <style> block that loaded it with @use got them in its output. The @property rules moved to base/properties, which the main bundle loads once, and the comments are Sass comments now. The compiled colorffy.min.css is unchanged.
  • Snackbar icons and actions are readable in both modes. Variant icons used --cffy-<color>-base-fixed, the light-mode brand tone, in both modes, so on the dark light-mode snackbar a primary icon measured 1.7:1 and danger 2.8:1, and on the white dark-mode snackbar warning measured 1.7:1, secondary 1.9:1 and info 2.3:1. Text buttons in the actions slot took the page's primary and text colors and nearly vanished. Icons now use --cffy-<color>-inverse (6.6:1 or more), and the snackbar points --cffy-primary-base, --cffy-on-background and the state layer at their inverse tones, so its actions read at 11:1 or more.
  • UiInputText renders a prefix or suffix added after mount. It decided whether the slots existed once, in a computed, and slots aren't reactive, so a #suffix shown with v-if later (a clear button, for example) never appeared. The template now reads the slots on every render.
  • Errors show under inputs with a prefix or suffix. The message follows the .input-group instead of the field, so the rule that styles it as an error never matched: it showed as plain body text. It now gets the error color and size there too.
  • UiProgressBar and UiProgressSpinner accept string styles. A string barStyles or customStyles was joined to the component's own style object, which turned it into [object Object], so the bar lost its fill width and the spinner its size. Both now pass a style array, which Vue merges, so strings and objects apply on top of the component's own values.
  • IToastDisplay includes title. UiAlertToast exposes its title, but the type for its template ref left it out, so setting the title before showToast() needed a cast in TypeScript.
  • UiInputColorPicker applies placeholder and readonly. Both props were accepted but never bound, so the hex field showed no placeholder and stayed editable. The hex field now takes both, and readonly also keeps the swatch's picker closed, since a color input ignores the readonly attribute.
  • Corner toasts fit their text and center on phones. The toast container is a size container (container-type: inline-size) so the alert inside can adapt to its width, but a corner placement left it without a width, so it measured 0px: every corner toast fell to its 288px minimum and took the stacked narrow layout, icon above the text. On phones it also slid left: the rule that spans the toast across screens up to 576px wide had less specificity than the placement classes, so a bottom-right toast kept its corner insets while gaining the side padding. Corner placements now give the container min(--cffy-container-lg, 100% - 2 × offset), and the phone rule covers every placement.
  • The color picker is one field. UiInputColorPicker wraps a swatch and a hex text field in .form-color-group, but the text field kept its own shadow, focus ring and invalid border, drawn in the field shape inside the group: with pill fields it showed as a second capsule, and an error turned only the text field red. The swatch was fixed at --cffy-radius-md whatever the field shape, and shrank when the field was narrow. The group now carries the shadow, the focus ring and the invalid border and ring, the text field draws none of them, the swatch rounds to half the field radius and keeps its size, and the error message gets the error color and size. The variant and rounded props, accepted but never applied, now style the whole field.
  • Connected button groups keep their shape with pill controls. The ends of .btn-group-connected round to half the button's height and the inner corners took --cffy-shape-control. Set to --cffy-radius-full, a side then asked for 9999px against a 32px box, and the spec answer to radii that overflow a side is to scale every corner of that button by the same factor: the pill ends shrank to nothing and the inner corners came out round, so the group looked inside out and its middle icon button turned into a circle. Inner corners now stop at a quarter of the button's height (8px on btn-sm, 9px at the default size, 12px on btn-lg), so the radii on a side never overflow it; below that they still follow the shape role.

2.8.0

Added

  • Duration tokens. --duration-100, -150, -200, -250, -300, -400, -500, -600 and -800 are milliseconds derived from --duration-unit (1ms), and every component transition reads them, so --duration-unit: .8ms speeds up all motion by 20%, globally or for one subtree. Long looping animations (spinners, shimmers) keep their own timing.

Changed

  • Ripples, the spinner and the galaxy gradient follow the theme.ripple-primary / ripple-secondary and the spinner's color cycle used the compiled $primary … $success colors, so they ignored your brand tokens and dark mode; they now use --theme-{color}-base. The white and dark ripples use the brand white and black tokens, and gradient-fuchsia starts and ends on --color-brand-accent-500.
  • The SCSS color and font variables are deprecated. $primary, $secondary, $accent, $success, $warning, $danger, $info, $muted, $primary-colors and $font-* are no longer read anywhere; they still compile so existing @forward … with configurations keep building, and they will be removed in 3.0. Set the --color-brand-* and --font-* tokens instead (see customization).

Removed

  • Private variables that never did anything: --_dialog-easing-out, --_dialog-easing-out-duration, --_navigation-bar-easing-out-duration, --_nav-tabs-gutter-y, --_shape-offset and --_container-gutter-y. They were declared but never read, so overriding them had no effect.

Fixed

  • Alerts with an icon stack in narrow containers. Below 350px the alert set --_alert-flex-direction: column, but .alert-content wrote flex-direction: row directly, so the icon never moved above the text. The content now reads the variable and left-aligns when stacked.
  • Tab colors go through the button variables. .tab-link set color and the hover background directly, overriding --_btn-color and --_btn-bg-color, so setting those had no effect. The rest, hover and active states now set the variables; the rendered colors are unchanged.
  • --_progress-color sets the progress bar fill, and --_table-bg-color / --_table-color set the table's background and text color. They were declared but never applied. Their defaults now match what rendered before (transparent and currentColor for the table).

2.7.0

Added

  • UiTabs takes fit and rounded. fit makes the tab bar only as wide as its tabs instead of spanning its container (ignored with fluid). rounded makes pill tabs and their indicator fully rounded; it only applies with pillTabs. Both radii come from one private variable, --_nav-tabs-item-radius.
  • UiAvatar and UiAvatarGroup take a color prop. It tints initials and placeholder avatars with any theme color, at the color's fixed tone and 25% opacity, in place of adding a bg-*-fixed class. On the group it applies to every avatar and the "+N" overflow avatar, and avatars entries can override it. The CSS classes are .avatar-{color}, driven by --_initials-base-color and --_initials-alpha.
  • UiAvatar and UiAvatarGroup take a variant prop. transparent is the see-through tint, tonal the same tint made opaque, and filled the full theme color with its on-color text (.avatar-tonal, .avatar-filled). UiAvatar defaults to transparent; UiAvatarGroup defaults to tonal.
  • border-neutral, border-black and border-opacity-*. border-neutral uses the body text color, so it flips with dark mode; border-black matches text-black. border-opacity-{0…100} fades any border color toward transparent with color-mix() in oklab.
  • Shape, weight, motion and focus tokens. --radius-*, --border-width-sm / -md, --fw-400…800, --ease-* and --focus-ring-color / --focus-ring-offset are CSS custom properties on :root, and every component reads them. Override them at runtime without recompiling; the SCSS variables still set their defaults. See customization.

Changed

  • Avatars in a UiAvatarGroup are opaque. Initials and the "+N" overflow avatar used a 25% transparent background, so the image they overlap showed through. The group now defaults to variant="tonal", the same tint made opaque; pass variant="transparent" for the previous look.
  • Easing curves no longer have cubic-bezier() fallbacks. linear() is supported in every current browser, so the 25 @supports blocks that swapped them in are gone and each component declares its curve once. Browsers without linear() support (before Chrome 113, Firefox 112 and Safari 17.2) now skip those transitions instead of using a simpler curve.
  • Reduced motion calms the overshooting curves. Under prefers-reduced-motion: reduce, --ease-spring, --ease-overshoot-soft and --ease-bounce resolve to --ease-decelerate, so dialogs, dropdowns, avatars and the rest of the components using them settle without overshooting or bouncing.
  • Font sizes are in rem, not px. The 17 remaining pixel font sizes (body text, buttons, tables, tooltips, badges, icons) render the same at the default browser size and now follow the user's font-size setting. If your site changes the root font size (e.g. html { font-size: 62.5% }), these now scale with it like the spacing and --fs-* tokens already did: body text set to .875rem renders at 8.75px with a 10px root.
  • Full-height layouts use dvh. The sidebar, dialogs, the PrimeVue dialog and .pane-content size themselves with dynamic viewport units, so mobile browser toolbars no longer push them past the visible area. The .vh-* utilities keep vh.
  • --fs-xs no longer shrinks below --fs-2xs. Its minimum was 0.60rem, smaller than --fs-3xs and --fs-2xs, so on narrow screens "xs" text rendered smaller than the steps below it. The minimum is now 0.75rem.
  • border-white and border-dark use the brand color tokens (--color-brand-white-500, --color-brand-dark-500), like text-white and text-dark, instead of compiled SCSS colors.

Fixed

  • Focused inputs are visible in forced-colors mode. Form controls drew their focus ring with box-shadow and removed the outline; forced-colors mode (Windows High Contrast) drops box-shadows, so focus disappeared. They now add a Highlight outline under @media (forced-colors: active).
  • The progress bar transition always has a duration.--_progress-easing-duration was only declared inside the linear()@supports block; it is now part of .progress's defaults.
  • border-animated glows without a gradient preset. Its glow is a conic-gradient() of --_border-gradient, which only the gradient-{name} classes set, so on its own the gradient was invalid, the background was dropped and nothing showed. It now defaults to the same primary gradient as border-gradient.
  • border-animated stops spinning under reduced motion. Its ::before and ::after ran angle-spin with no prefers-reduced-motion check, unlike border-gradient-spin. Under prefers-reduced-motion: reduce the animation is now none, so the glow stays still at its starting angle.

2.6.1

Fixed

  • Shimmer skeletons render again. .skeleton-shimmer built its gradient from --theme-muted-a5, a token that doesn't exist, so the whole background was dropped and the skeleton was invisible. The highlight is now the muted tone at half opacity.
  • The segmented control's easing variable is spelled correctly. It was declared as --_sc_easing-in but read as --_sc-easing-in, so browsers without linear() support got no transition.
  • The side-sheet dialog's fallback animation uses an easing curve. Its animation-timing-function read --_dialog-easing-duration (a duration) instead of --_dialog-easing-in.
  • The active menu-tools subtitle in the navbar is primary-colored. It referenced --theme-primary-a0, which doesn't exist; it now uses --theme-primary-a10.
  • The navigation bar's reduced-motion indicator fades with its own easing. It referenced the undefined --easing-in / --easing-out; it now uses --_navigation-bar-easing-in / --_navigation-bar-easing-out.
  • Private variables that were declared but ignored now apply:
    • --_drawer-radius sets the sidebar's corners. It held the wrong corners and the radius was written directly; drawer-bordered sets it to 0.
    • --_dropdown-shadow sets the dropdown panel's shadow.
    • --_table-border-color sets every table border.
    • --_form-check-input-active-border-color (previously the misspelled --_form-check-input-activeborder-color) sets a checked input's border.

    Their defaults match what rendered before, so nothing changes visually unless you override them.
  • .icon-wrap no longer emits an invalid border.--_icon-wrap-border-size defaulted to none, which isn't a width; it is now 0.

2.6.0

Added

  • UiTabs takes a size prop. size="sm" adds .tabs-sm, which puts the tab buttons back on the default button height, padding, icon size and font size instead of the larger tab defaults. md (or leaving it unset) keeps the current size.
  • The btn-size mixin has an md step. It emits the default .btn sizing, so a component that enlarges its buttons can reset them in one line; .tabs-sm uses it.
  • UiDatatable toolbar slots. #actions-start and #actions-end add your own buttons to the built-in column button group, before and after it. #column-toggle and #column-manager replace the built-in buttons and receive the column state (columns, allVisible, isVisible, isLocked, toggle, toggleAll, typed as IDatatableColumnSlotProps), so a custom control drives the same visibility logic.
  • UiDatatable takes a toolbarButton prop. One object (variant, color, size, customClass, rounded) restyles both built-in toolbar buttons, instead of a prop per button per option.
  • UiDatatable takes a stickyHeight prop. It sets the sticky scroll container's max height (sticky-height="18rem"; a number is read as pixels), replacing the inline --_table-sticky-max-height style binding. The variable still works and still defaults to 32rem.

Changed

  • UiDatatable's column manager is an icon-only button. It shows the view_column icon with the same outline, small, shape-sm style as the column toggle beside it; columnManagerTooltip is its tooltip and accessible name.
  • UiInputFile reads the shared size prop. size="lg" renders the taller dropbox. The input already inherited size from the base input props but ignored it, so the dropbox had its own large boolean; large still works but is deprecated in favour of size="lg".

Removed

  • UiDatatable's columnManagerText prop. The column manager button has no label anymore; drop the prop, or move the wording to columnManagerTooltip.

Fixed

  • Text-only tabs are no longer wider on the right. .tab-link kept the button grid's two columns and a --space-4 gap even with nothing in the second column, adding 4px after the label; a tab with an icon but no badge got the same phantom gap. Tab links now lay out with flex, so the gap only sits between items that exist. As a side effect, fluid tabs with an icon centre the icon, label and badge together instead of pinning the icon to the left edge.
  • UiDatatable's toolbar renders for #controls alone. It only rendered when a column started hidden or column-manager was on, so a table using just the #controls slot showed nothing. Any toolbar slot now renders it.
  • UiDatatable toolbar ids are unique per table. The column toggle, column manager and each menu item used fixed ids (columns-toggle, column-manager, column-<key>), which collided with two tables on one page. They are now prefixed with a per-instance id.
  • UiButtonTooltip and UiButtonMenu no longer share ids.UiButtonTooltip defaulted id to 'tooltip', so every instance without an id rendered button-tooltip; UiButtonMenu built -dropdown and -tooltip popper ids from an empty id. Both now default to no id and leave the popper id to floating-vue unless you pass one, like UiLinkTooltip.
  • UiDatatable's default empty-state subtitle reads correctly. "Try may want to try…" is now "You may want to try…".

2.5.1

Added

  • UiFooter, UiFooterGroup and UiFooterItem. The footer styles existed in @colorffy/css with nothing rendering them; these cover the brand block, the link groups (column or row) and the links themselves, including the plain .anchor-link entry for a group member with no target. UiFooter supplies its own container and takes the same fluid prop as UiNavbar, plus a #bottom slot for the divided bar under the groups; UiFooterGroup takes overline for a small uppercase heading.
  • UiHeroContent. A page-opening hero: eyebrow, display-sized title, description and a #actions slot for CTAs, with size selecting the display step and align moving the whole block.
  • UiSubheadingContent. A section subheading with an optional description, built on .text-subheading, with as to keep the heading order valid, a gutter prop for the space below it, and an #actions slot for a badge or control beside it.

Changed

  • .text-subheading uses the type tokens. It picks up --fs-lg--line-height and the --space-* gutters, and no longer runs the text-variable mixin: it sets --font-primary directly, which every font honours.
  • Chips split the container from the selected color. .btn-chip is the outline container and .chip-elevated the raised one; the color classes (.chip-primary, .chip-secondary, .chip-neutral) only apply together with .chip-active. .a40 and .a60 are removed, and .chip-active no longer forces a pill radius. See the migration note.
  • UiHeaderContent renamed hideActionsOnMobile to hideActionsWhenNarrow, defaulting to false. The old name described a device, but the mechanism is a container query: a narrow header collapses inside a wide window too. The old default also destroyed the page's primary actions below 700px unless you opted out, so hiding them is now something you opt in to; otherwise they wrap below the title.
  • UiHeaderContent emits back instead of click. Declaring click as an emit meant a consumer's @click never attached as a native listener — it fired only for the back button. @click now behaves like any other native listener, and @back carries the intent.
  • UiHeaderContent's containerClass accepts ClassValue. It previously took string | string[] and stringified the object form to [object Object]. Existing values keep working.
  • UiPaneContent's customClass and containerClass accept ClassValue. Same widening as UiHeaderContent; existing values keep working.
  • The page header's back-navigation class is .page-header-back. It was .page-header-breadcrumb, which described a component the header does not render. The back button is also centred against the title block now, instead of aligning to its top edge.
  • The header's bottom margin moved to .header-container and has its own token. --_header-block-end and --_header-back-gap split out of --_header-columns-gap, which was setting the title/actions gap and the space below the header at once. The margin sat on the inner .header, so a padded or tinted container trapped it inside its own box and a mb-* utility on the container stacked on top of it. On the container it is a single margin that utilities override, since utilities is layered after components.
  • UiHeaderContent takes an as prop. The title was always an h1, so a page using the component for its section headers emitted several. It defaults to h1; pass as="h2" and below for sections.
  • UiHeaderContent takes a headline prop. It renders a small eyebrow label above the title with caption text-primary.
  • UiHeaderContent takes a size prop. sm (the default, --fs-xl) through 2xl (--fs-5xl), each step also applying the line height the type scale pairs with it. UiHeroContent uses the same sm–xl vocabulary on the display scale, so one size language covers both.
  • The header title no longer uses the text-variable mixin, and its 992px breakpoint is gone. The mixin pinned the title to wght/GRAD/wdth variation axes that most families do not ship, and swapped the typeface behind an @supports test; $type-h1 already gives the h1 the same family and weight in a way every font honours. The breakpoint then swapped --fs-xl for --fs-lg, but the --fs-* scale is already fluid, so the title shrank twice and landed at 16px on a phone. It now scales once, 20px to 24px.
  • UiHeaderContent and UiHeroContent take a viewTransitionName prop. The header hard-coded view-transition-name: selected-header on every title and selected-description on every description. A name has to be unique per document, so a page with more than one header — a page title plus its section headers — made the browser skip the transition entirely rather than run it. The name is also the key pairing two pages, which only the app knows, so it is now passed in and nothing is emitted without it. contain: paint and the fit-content inline size come with it instead of applying to every header.
  • The footer's gradient underlay, hover radius and rounded top corners, and .dialog-highlight. Both drew a gradient through a pseudo-element; the footer also squared its top corners on hover, and its corners are now flat with no radius transition left to animate.
  • .footer-row declares its own direction. It set a custom property the base rule never read, so a row group was horizontal only because that is the flex default. A row group with a title now keeps the heading on its own line instead of inline with the links.
  • Card surfaces are static and interaction is its own class..card-outline, .card-pane and .card-elevated now only describe the surface; .card-selectable and .card-link carry every hover, press and focus state, including the elevated card's sm → md → lg shadow escalation. A plain elevated card previously animated its shadow with nothing clickable behind it. .card-link also picks up the same background feedback as a selectable card, which it had diverged from. See the migration note.
  • The selectable card keeps its variant's border and its own radius. It set a 2px transparent border to reserve room for the selected ring; with box-sizing: border-box the outer box never moved anyway, so the ring is a 1px border that matches a static .card-outline beside it. The selected and pressed states also hard-coded $rounded-lg / $rounded-xl, which ignored a configured $card-border-radius — on a 4px theme, clicking a card ran it 4px → 25px → 12px and left it there. The radius no longer changes.
  • The selectable card's ring answers :focus-visible. It was :focus, so a mouse click left the ring on the card until it lost focus.
  • The selectable card's hover lift is 4px. It was 8px, which read as a drag rather than a hover; card-sm steps down to 2px alongside it.

Removed

  • UiHeaderContent's actions prop. Filling the #actions slot is the signal; passing the slot without the flag used to render nothing, silently.
  • .subheading-m2. It set the same gutter .text-subheading already defaults to, so it never changed anything, and the reference documented it as a "large margin" it could not produce. subheading-m1 and subheading-m0 stay for a tighter gutter or none.
  • @colorffy/ui's ./style.css export. The package no longer emits a stylesheet: its one scoped style block moved into @colorffy/css, which every consumer already loads. Nothing referenced the export, and it now pointed at a file that is not built.
  • .page-header-featured and the header's .col-heading / .col-actions rules. None of them had any markup behind them: the featured badge block styled a composition no component rendered, and the two media queries targeted classes that exist nowhere. The back-navigation and actions layouts are unaffected.

Fixed

  • subheading-m0 / m1 now move the space below the block. The modifiers set --_text-subheading-gutter on the heading, and a custom property never reaches a sibling, so the description below kept its default margin whichever modifier was applied. Each modifier now also sets the description's margin.
  • The title-to-description gap no longer follows the gutter. It was half the gutter, so tightening the space below the block squeezed the title against its own description — 2px at subheading-m1. It is now its own --_text-subheading-gap, fixed at --space-8.
  • .card-selectable no longer erases the variant's border..card-outline.card-selectable and .card-frosted.card-selectable matched at the same specificity, and .card-selectable came last in the file, so its --_card-border-color: transparent won and the card rendered with no outline at all until it was pressed or selected. It no longer declares the colour at rest, which the base .card already defaults to transparent.
  • An interactive card's transitions all run. .card-selectable and .card-link each declared their own transition, so on a card that was both — which UiCard produces whenever selectable meets to/href — the later rule won and the lift and the ring never animated. box-shadow was also missing from the selectable list, so an elevated card jumped straight to its hover shadow. One shared declaration now covers every interactive card. --_card-easing-in also has its non-linear() fallback back: it was only defined inside the @supports block, and an undefined custom property in a transition makes the whole declaration invalid at computed-value time, so without linear() support the card had no transition at all.
  • A card-pane card gives hover feedback in light mode. The shared hover steps the background to --theme-surface-overlay, which resolves to the same white as the pane's own surface in a light theme — an interactive pane card changed nothing under the pointer. Pane now steps to --theme-surface-a30, which contrasts in both schemes, and carries its own press step so :active is not outranked by it.

The page header title scales down again

.header stepped its title down to --fs-lg below 992px through :where(.col-heading h1:is(.text-title)) — a selector that matched nothing, and whose :where() wrapper would have given it zero specificity even if it had. The size now flows through --_header-title-size, overridden inside the .header rule itself, so it carries the same specificity as the value it replaces.

The header's view transitions actually apply

view-transition-name, contain: paint and the fit-content inline size for .text-title / .text-description lived in a Vue scoped style block that compiled to @colorffy/ui's dist/ui.css — a file nothing imported, so the declarations never reached the page. They now live here in _header.scss alongside the rest of the header rules.

Connected groups no longer flatten their inner corners

The rounded end of .btn-group-connected came from a fixed --_btn-connected-radius: 50px. On a 32px-tall button that is 50px + 50px down one side against a 32px box, and the spec answer to radii that overflow a side is to scale every corner of that element by the same factor — here 32 / 100. The first button's inner corners, asking for the 4px of --_btn-radius, rendered at 1.3px and read as square next to their siblings:

before   first: 50, 1.3, 1.3, 50   middle: 4, 4, 4, 4
after    first: 16, 4,   4,   16   middle: 4, 4, 4, 4

The radius is now derived from the button instead of hard-coded:

.btn-group-connected .btn {
  --_btn-connected-radius: calc(var(--_btn-height) / 2);
}

The two radii on a side then sum to exactly --_btn-height, which min-height guarantees is never taller than the button, so the scale factor stays 1 and the inner corners keep their own value. It also tracks the size variants — btn-sm, btn-lg and btn-icon each get the pill that fits them.

If you set --_btn-connected-radius yourself, move it onto the buttons (.btn-group-connected .btn) — a declaration on the group is now shadowed by the one above.

Vertical connected groups follow the button radius

.btn-group-vertical.btn-group-connected had three separate mismatches: middle buttons were pinned to $button-border-radius instead of --_btn-radius, so a group of btn-sm got 8px corners where the horizontal variant gave 4px; the first and last button squared their inner corners to 0, although the group is separated by a gap; and the joined variant set border-radius: 0 as a shorthand that outranked the end rules, so vertical joined groups lost their pill ends entirely. All three now behave like the horizontal variant.

2.5.0

Changed

  • Every spacing and font-size value moved onto :root design tokens, with the old names kept as deprecated aliases. See the migration guide.
  • Tabs draw their active state with an anchored indicator element, the popover menu renders as a native popover, and its default header no longer draws the avatar block. See new in 2.5.
Copyright © 2026