Migration
Each section covers one upgrade, newest first, and lists only what needs an edit on your side. Everything else a release changes is in the changelog.
Upgrading to 3.0
3.0 removes what 2.x deprecated, namespaces every public custom property with
--cffy-, and settles the component API. Work through the steps in order: the
find-and-replace in step 2 expects step 1 to be done.
- Clear the 2.x deprecations
- Namespace the custom properties
- Update tonal colors
- Replace the removed styles
- Replace removed props, slots and components
- Rename event listeners
- Use the ids you pass
- Replace PrimeVue components (optional)
Steps 1–4 touch your stylesheets (@colorffy/css), steps 5–7 your templates
(@colorffy/ui). Step 8 is for apps that still use PrimeVue, whose Colorffy
skin 3.0 removes. Until you have finished, keep both packages on ^2.8, which
still accepts every deprecated name and still styles PrimeVue.
Clear the 2.x deprecations
These names still work in 2.8, so you can make these edits and ship them before you upgrade.
Font sizes. The ordinal tokens and their classes are gone. Rename them to the t-shirt scale:
| Removed token / class | 3.0 token | 3.0 class | Max size |
|---|---|---|---|
--fs-sm-500 / .fs-sm-500 | --cffy-fs-4xs | .fs-4xs | 11px |
--fs-sm-400 / .fs-sm-400 | --cffy-fs-3xs | .fs-3xs | 12px |
--fs-sm-300 / .fs-sm-300 | --cffy-fs-2xs | .fs-2xs | 13px |
--fs-sm-200 / .fs-sm-200 | --cffy-fs-xs | .fs-xs | 14px |
--fs-sm-100 / .fs-sm-100 | --cffy-fs-sm | .fs-sm | 15px |
--fs-600 / .fs-600 | --cffy-fs-base | .fs-base | 16px |
--fs-500 / .fs-500 | --cffy-fs-lg | .fs-lg | 20px |
--fs-400 / .fs-400 | --cffy-fs-xl | .fs-xl | 24px |
--fs-300 / .fs-300 | --cffy-fs-2xl | .fs-2xl | 28px |
--fs-200 / .fs-200 | --cffy-fs-3xl | .fs-3xl | 32px |
--fs-100 / .fs-100 | --cffy-fs-4xl | .fs-4xl | 40px |
--fs-xl-100 / .fs-xl-100 | --cffy-fs-5xl | .fs-5xl | ~53px |
On 2.8 the new tokens have no prefix yet (--fs-4xs); step 2 adds it. A
t-shirt class also sets its paired line-height, which the legacy class did not,
so text moved from .fs-500 to .fs-lg gets a 28px line box. Check tight
layouts such as badges and table cells.
SCSS variables. $space-1, $space-2 and $space-3 are gone; use
var(--cffy-space-16), var(--cffy-space-32) and var(--cffy-space-48). So
are the color and font variables deprecated in 2.8 ($primary … $muted,
$primary-colors, $font-*), and a @forward … with (…) that still sets one
fails to compile. Drop them from the configuration and set the tokens instead:
// 2.x
@forward '@colorffy/css/scss/abstracts/variables' with (
$primary: #6750a4,
$font-primary: 'Inter', sans-serif
);
/* 3.0 */
:root {
--cffy-color-brand-primary-500: #6750a4;
--cffy-font-primary: 'Inter', sans-serif;
}
Namespace the custom properties
Every public custom property now starts with --cffy-, so Colorffy's names no
longer collide with other libraries on the page. The semantic --theme-*
tokens swap theme- for the namespace (--theme-primary-a10 →
--cffy-primary-a10, --theme-surface-container → --cffy-surface-container),
and every other public name gains it (--color-brand-primary-500 →
--cffy-color-brand-primary-500, --space-16 → --cffy-space-16,
--card-bg-color → --cffy-card-bg-color). In your stylesheets and inline
styles, run these replacements in order:
- Replace
--theme-with--cffy-. - Replace this regular expression with
--cffy-$1. It only matches names that are Colorffy's alone, so your own custom properties stay untouched:(?<![\w-])--((?:accordion|alert|anchor|avatar|badge|breadcrumb|btn|card|carousel|chip|code|dialog|divider|dots|dropdown|duration|element|fab|fade|footer|form|fs|fw|gradient|header|hero|icon|ie|input|iw|kbd|link|list|loading|menu|navbar|navigation|noise|popover|progress|pwa|segmented|shape|sidebar|skeleton|space|state|stepper|subheading|table|tabs|timeline|toggle|tooltip|vfs|vfw|color-brand|color-wheel|border-width|focus-ring)-|angle\b|carousel\b|h-fixed\b|w-fixed\b) - Review
--radius-*,--shadow-*,--ease-*and--font-primary/-secondary/-code/-iconsby hand. Tailwind v4 and other libraries use the same names, so rename only the ones that set or read Colorffy's tokens.
These variables changed more than their prefix:
| 2.x | 3.0 |
|---|---|
--theme-nav-drawer-width (--cffy-nav-drawer-width after the replacement) | --cffy-sidebar-width |
--_drawer-active-color | --cffy-sidebar-item-active-icon-color |
--_progress-width (the fill of a hand-written .progress-bar) | --cffy-progress-value |
UiSidebar's width prop already sets the new name.
Private variables. Variables that start with --_ were never public API,
and in 3.0 most of them are named after their public hook
(--_card-gutter-x → --_card-padding-inline, --_drawer-* → --_sidebar-*,
--_form-* → --_input-*). A rule that set an old name stops applying without
an error. Search your stylesheets for --_ and set the component's public hook
instead, which also survives future refactors:
/* 2.x */
.compact-card { --_card-gutter-x: var(--space-8); }
/* 3.0 */
.compact-card { --cffy-card-padding-inline: var(--cffy-space-8); }
Each component page lists its hooks under CSS variables.
Update tonal colors
Tinted text reads the container tokens. In dark mode a90 used to be a
light tint, the text color for tinted surfaces. Every ramp now darkens step by
step in both modes, so a90 is its darkest step, and that text role is
--cffy-on-<color>-container. Where you read --cffy-<color>-a90 for text or
icons on a tonal background, read the container token instead:
/* 2.x */
.tag-success { color: var(--theme-success-a90); }
/* 3.0 */
.tag-success { color: var(--cffy-on-success-container); }
Light mode renders the same either way. .text-*-emphasis and the library's
components already read the new token.
Inverse on-colors are gone. --cffy-on-primary-inverse,
--cffy-on-secondary-inverse and --cffy-on-accent-inverse were black in light
mode and white in dark mode; read --cffy-on-background instead.
Custom intensities. --cffy-tonal-dark-intensity now defaults to 58% and
--cffy-tonal-light-intensity to 8% (were 70% and 15%), because the ramps
mix in oklab. A value you set yourself comes out darker in dark mode than it
did in 2.x; start again from the new defaults.
Tonal mixins take the surface (SCSS). btn-tonal() and container-tonal()
no longer compute the container from the base tone and an $isDark flag. Pass
the container token, or build one with the tonal() function. btn-tonal()
also takes the fill that hover and pressed mix toward:
// 2.x
@include m.btn-tonal(var(--theme-success-a10), var(--theme-success-a90), true);
@include m.container-tonal(var(--theme-success-a10), var(--theme-success-a90), true, false, badge);
// 3.0
@include m.btn-tonal(var(--cffy-success-container), var(--cffy-success-a10), var(--cffy-on-success-container));
@include m.container-tonal(var(--cffy-success-container), var(--cffy-on-success-container), false, badge);
Replace the removed styles
Some rules only ever served one app, or nothing at all, and are gone from
@colorffy/css. Edit your own markup or build only if it used one:
| Removed | Use instead |
|---|---|
.menu-tools-container mega menu (.menu-tools, .menu-tabs, .menu-grid, .menu-cta) | Nothing replaces it. Copy its rules from @colorffy/css@2.8 into your own stylesheet, or build the panel with UiPopover |
.segmented-control (with .segmented-control-active and .active-grid) | UiSegmentedControls, or .tab-segmented-control in CSS-only markup |
.pwa-alert-container (.pwa-content, .pwa-content-text-wrapper, .pwa-actions) | UiAlert with type="snackbar" and actions, or copy the rules from @colorffy/css@2.8 |
.ripple, .ripple-dark, .ripple-primary, .ripple-secondary | Nothing; the ripple-color($color) mixin stays, so you can rebuild the effect |
The PrimeVue skin is gone. @colorffy/css no longer styles PrimeVue's
.p-multiselect, .p-select, .p-colorpicker, .p-toast,
.p-confirm-dialog and .p-inputswitch, or sets its --p-* tokens. Move to
the native fields in step 8. If some PrimeVue
components have to stay for now, copy scss/components/_prime.scss from
@colorffy/css@2.8 into your app's styles and import it after the framework.
Small helpers. Nothing in Colorffy UI used the rest, so copy any you rely on into your own stylesheet:
.hover-rotateand.hover-scale,.vr, and thepulse,glitch,shake-x,shake-y,fade-in-leftandslide-in-downkeyframes.- The
slide,slide-inline,fade-inline,tableandtable-td-fadetransitions;slide-blockstays. The new transitions cover most of them: renameslide-inlinetoslide-start,fade-inlinetoslide-endandtabletolist.
Replace removed props, slots and components
| Component | Removed | Use instead |
|---|---|---|
UiPopoverMenu | user, avatarUrl, avatarCustomClass, subtitle | UiPopoverMenuUser in #header, see the header no longer renders identity |
UiPopoverMenu | #body-extra slot | Render the whole body in #body (below) |
UiPopoverMenu | Default slot | <template #body> |
UiSidebar | Default slot, UiSidebarHeader, UiSidebarBody, UiSidebarFooter | #header, #body and #footer slots (below) |
UiEmpty | #button slot | #action |
UiInputFile | large | size="lg" |
BaseSkeleton | isThumbnail | variant="thumbnail" |
ISegmentedTab | position | Nothing; delete it, the indicator measures the active tab |
UiButtonToggleGroup | groupLabel | ariaLabel; groupLabel never rendered |
UiModal | isHeadless | mode="headless"; isHeadless never applied |
UiButton | tooltipText, placement | UiButtonTooltip; UiButton never showed a tooltip |
UiPopoverMenu | auth | Nothing; delete it, it was never read |
UiDatatable | skeletonCols, skeletonColExpanded, isExpanded | Nothing; the loading skeleton matches the visible columns |
Adding rows to a menuItems menu. #body-extra appended content after the
generated items. Render the items yourself in #body instead:
<!-- 2.x -->
<UiPopoverMenu :menu-items="items">
<template #body-extra>
<ThemeSwitch />
</template>
</UiPopoverMenu>
<!-- 3.0 -->
<UiPopoverMenu>
<template #body>
<UiPopoverMenuGroup>
<UiPopoverMenuItem v-for="item in items" :key="item.id" v-bind="item" />
</UiPopoverMenuGroup>
<ThemeSwitch />
</template>
</UiPopoverMenu>
Sidebar regions. UiSidebar's slots render the header, body and footer
themselves, the way UiCard and UiModal do. Move each region into its slot,
and its custom-class to header-class, body-class or footer-class:
<!-- 2.x -->
<UiSidebar>
<UiSidebarHeader custom-class="px-2">…</UiSidebarHeader>
<UiSidebarBody>…</UiSidebarBody>
<UiSidebarFooter>…</UiSidebarFooter>
</UiSidebar>
<!-- 3.0 -->
<UiSidebar header-class="px-2">
<template #header>…</template>
<template #body>…</template>
<template #footer>…</template>
</UiSidebar>
Rename event listeners
Events lost their on prefix, and tab and step selection now emit the
update: events v-model listens to:
| Component | 2.x | 3.0 |
|---|---|---|
UiButton, UiButtonMenu, UiButtonTooltip | @on-click | @click |
UiInputText, UiInputTextarea, UiInputSelect, UiInputCheck, UiInputRadio, UiInputRange, UiInputFile, UiInputOtp, UiInputPhoneNumber, UiInputColorPicker | @on-update | @update |
UiModal, UiConfirmModal | @on-close-dialog | @close |
UiButtonToggleGroup | @on-option-click | @option-click |
UiTabs, UiSegmentedControls | @update-active-tab | v-model:active-tab, or @update:active-tab |
UiStepper | @update-active-step | v-model:active-step, or @update:active-step |
A listener that only stored the new value becomes a v-model:
<!-- 2.x -->
<UiTabs :tabs="tabs" :active-tab="tab" @update-active-tab="tab = $event" />
<!-- 3.0 -->
<UiTabs v-model:active-tab="tab" :tabs="tabs" />
@click on a button already worked through the native event and keeps
working; it now also receives the MouseEvent, and a disabled or loading button
never emits it. Search your templates for @on-, v-on:on-,
@update-active- and camelCase forms such as @onClick to find the rest.
Use the ids you pass
The id prop now lands unchanged on the component's main element. In 2.x some
components rewrote it:
| Component | 2.x rendered | 3.0 renders |
|---|---|---|
UiButton | button-<id> | <id> |
UiCard | card-<id> | <id> |
UiAccordion | accordion-<id> | <id> |
UiLinkTooltip | link-<id> | <id> |
UiInputText, UiInputPhoneNumber | <id>-input-text | <id> |
UiInputTextarea | <id>-input-textarea | <id> |
UiInputSelect | <id>-select | <id> |
UiInputRange | <id>-input-range | <id> |
UiInputColorPicker | <id>-input-color, and <id>-input-text on the text field | <id>, and <id>-text on the text field |
Update the selectors, <label for> attributes, # links, getElementById()
calls and test queries that used the generated names:
// 2.x
document.getElementById('email-input-text')?.focus()
// 3.0
document.getElementById('email')?.focus()
Error messages keep their ids (<id>-error-0), so aria-describedby still
points at them.
Menu buttons have no default tooltip
UiButtonMenu showed a "menu" tooltip unless you passed tooltip-text. It now
shows none, so add tooltip-text where you want one:
<UiButtonMenu icon tooltip-text="Project actions">…</UiButtonMenu>
An icon-only menu button without tooltip-text or title is named "Menu" by
the buttonMenu.ariaLabel label; set tooltip-text or title to give it a
name that says what the menu does.
Replace PrimeVue components
This step is optional. 3.0 has native replacements for PrimeVue's Select, MultiSelect and DatePicker, the components the Colorffy skin styled, and for AutoComplete:
| PrimeVue | Colorffy UI |
|---|---|
Select (Dropdown before v4) | UiInputCombobox |
MultiSelect | UiInputMultiSelect |
AutoComplete | UiInputCombobox with remote, or UiInputMultiSelect for multiple |
DatePicker (Calendar before v4) | UiInputDate, or UiCalendar for inline |
All of them are complete fields: they take label, errorMessages and the
shared input props, so the
FloatLabel or IftaLabel wrapper and the separate error message go away.
Their lists and calendars open in the top layer, above dialogs and outside
clipping containers, so appendTo has no equivalent.
Select
<!-- PrimeVue -->
<FloatLabel>
<Select v-model="leadId" input-id="lead" :options="members" option-label="name" option-value="id" filter show-clear />
<label for="lead">Project lead</label>
</FloatLabel>
<!-- Colorffy UI -->
<UiInputCombobox id="lead" v-model="leadId" label="Project lead" :options="members" option-label="name" option-value="id" clearable />
| Select | UiInputCombobox |
|---|---|
options, optionLabel, optionValue, optionDisabled | The same props |
optionGroupLabel and optionGroupChildren (nested options) | optionGroup, a field on flat options |
filter | Filtering is on by default; :filterable="false" for a plain select |
showClear | clearable |
emptyMessage, emptyFilterMessage | emptyText, or the #empty slot |
invalid | errorMessages |
variant="filled", size | variant="filled", size |
#option slot | #option="{ option, selected, active }" |
@change | @update, or watch the v-model |
editable | freeText |
loading | loading |
#value slot, virtual scrolling | Not available yet |
MultiSelect
The option props are the same as on Select. The rest maps like this:
| MultiSelect | UiInputMultiSelect |
|---|---|
display="chip" | The default: a removable chip per value |
maxSelectedLabels | maxChips |
selectedItemsLabel ('{0} items selected') | maxChipsLabel ('{count} selected') |
selectionLimit | max |
@change | @add and @remove with the value, or @update |
showToggleAll | Not available |
AutoComplete
PrimeVue asks for suggestions with @complete and shows suggestions; the
Colorffy fields emit search with remote and show options.
| AutoComplete | UiInputCombobox with remote |
|---|---|
suggestions | options |
@complete (event.query) | @search (the text) |
delay, minLength | searchDelay, minSearchLength |
| Typed text kept as the value | freeText |
forceSelection | The default without freeText |
loading, loader | loading |
dropdown | The toggle button is always there |
optionLabel, #option | The same |
multiple | UiInputMultiSelect with remote, plus freeText for typed values |
DatePicker
The model is the change to watch: PrimeVue holds a range as [start, end],
Colorffy as { start, end }, and only writes it once both ends are picked.
<!-- PrimeVue -->
<DatePicker v-model="period" selection-mode="range" :number-of-months="2" show-time :min-date="minDate" show-icon />
<!-- Colorffy UI -->
<UiInputDate id="period" v-model="period" label="Period" mode="range" time :min="minDate" />
When an API sends or expects [start, end], convert where you read it:
import type { IDateRange } from '@colorffy/ui'
const range = computed<IDateRange | null>({
get: () => (period.value ? { start: period.value[0] ?? null, end: period.value[1] ?? null } : null),
set: (value) => { period.value = value ? [value.start, value.end] : null },
})
| DatePicker | UiInputDate |
|---|---|
selectionMode="single", "range" | mode="single", "range" |
selectionMode="multiple" | mode="multiple", with a Date[] model |
inline | UiCalendar |
numberOfMonths | months (1 for a date, 2 for a range by default) |
minDate, maxDate | min, max |
disabledDates (an array) | disabledDates, a function: date => booked.some(day => day.toDateString() === date.toDateString()) |
disabledDays (weekday numbers) | disabledDates: date => [0, 6].includes(date.getDay()) |
firstDayOfWeek (locale config) | weekStart, which defaults to the locale's |
showTime, showSeconds, stepMinute | time, time="seconds", minuteStep |
hourFormat | The browser's clock: the time fields are native <input type="time"> |
dateFormat ('mm/dd/yy') | format (Intl.DateTimeFormat options); typed dates follow the locale's order |
showIcon, manualInput | Always on: a calendar button and typed dates. trigger="button" gives a button without typing |
showButtonBar (Today and Clear) | presets with datePresets.today(), and clearable |
touchUI | Automatic: a bottom sheet below 600px |
showOtherMonths | showOutsideDays on UiCalendar (shown, not pickable) |
@date-select | @update, or select on UiCalendar |
view="month", view="year", timeOnly | Not available |
Remove PrimeVue
Once no PrimeVue components are left, delete the PrimeVue plugin
(app.use(PrimeVue), or @primevue/nuxt-module in nuxt.config), its theme
setup and any copied _prime.scss, then uninstall primevue and
@primevue/themes.
2.0 → 2.5
2.5 moved every spacing and font-size value onto design tokens. Nothing broke: the old names kept working as deprecated aliases until 3.0.
New spacing tokens
Spacing now uses a value-encoded numeric scale — the number is the pixel value at the default 16px root — derived from a single unit:
--space-unit: .25rem;
--space-4 /* 4px */ --space-16 /* 16px */
--space-6 /* 6px */ --space-20 /* 20px */
--space-8 /* 8px */ --space-24 /* 24px */
--space-12 /* 12px */ --space-32 /* 32px */
--space-14 /* 14px */ --space-48 /* 48px */
All components consume these tokens, so you can retheme spacing at runtime:
/* denser UI everywhere, proportionally */
:root { --space-unit: .2rem; }
/* or adjust one step */
:root { --space-12: .7rem; }
m-* / p-* / gap-*utility classes keep their own historical scale
(1 = 0.25rem … 10 = 12rem) in 2.5 and are unaffected by these tokens.Font sizes renamed to a t-shirt scale
The old ordinal names (--fs-100, where a smaller number meant a bigger
size) are replaced by a t-shirt scale anchored at --fs-base. Values are
unchanged — same fluid clamp()s, new names:
| Deprecated (2.x alias) | New token | Max size |
|---|---|---|
--fs-sm-500 | --fs-4xs | 11px |
--fs-sm-400 | --fs-3xs | 12px |
--fs-sm-300 | --fs-2xs | 13px |
--fs-sm-200 | --fs-xs | 14px |
--fs-sm-100 | --fs-sm | 15px |
--fs-600 | --fs-base | 16px |
--fs-500 | --fs-lg | 20px |
--fs-400 | --fs-xl | 24px |
--fs-300 | --fs-2xl | 28px |
--fs-200 | --fs-3xl | 32px |
--fs-100 | --fs-4xl | 40px |
--fs-xl-100 | --fs-5xl | ~53px |
Every size also gained a line-height companion using the double-dash suffix convention:
.intro {
font-size: var(--fs-lg);
line-height: var(--fs-lg--line-height); /* 1.4 */
}
New font-size utility classes
The new .fs-4xs … .fs-5xl classes set the size and its paired
line-height. The legacy classes (.fs-100….fs-600, .fs-sm-100….fs-sm-500,
.fs-xl-100) still work and still set font-size only, so existing markup
renders identically.
<!-- 2.0 (still works, deprecated) -->
<p class="fs-500">Title</p>
<!-- 2.5 -->
<p class="fs-lg">Title</p>
Deprecated in 2.5 (removed in 3.0)
- The 12 old font-size token names (aliases of the new tokens).
- The 13 legacy
.fs-*utility classes. - The
$space-1/$space-2/$space-3SCSS variables — usevar(--space-16/32/48).
New in 2.5 (breaking for hand-written markup)
Tabs need an indicator element
.tabs-navigation no longer draws the active state as a border on the active
link. The active link publishes an anchor-name and a dedicated element is
positioned over it with CSS anchor positioning, so the list needs one extra
child as its last item:
<ul class="tabs-navigation">
<li class="tab-item">
<button class="tab-link active">Overview</button>
</li>
<li class="tab-item">
<button class="tab-link">Pricing</button>
</li>
<!-- required: the anchored indicator -->
<li class="tab-indicator" aria-hidden="true" role="presentation"></li>
</ul>
Without that element the active tab shows no underline (or pill, with
.tabs-pills) in browsers that support anchor positioning. Browsers that do
not support it fall back to the old border/background on the active link, so
they look the same either way.
This affects @colorffy/css consumers writing their own markup only —
UiTabs in @colorffy/ui renders the element for you. The same pattern was
already in place for .tab-segmented-control, whose .pill-indicator child
is unchanged.
The popover menu header no longer renders identity
UiPopoverMenu's default header is now a title and the close button, with the
close button pinned to the top right. Passing user, avatarUrl,
avatarCustomClass or subtitle no longer draws the avatar block — move it
into the header slot, where UiPopoverMenuUser takes the same user object
and lays the avatar beside the name and email:
<!-- before -->
<UiPopoverMenu :user="user" avatar-custom-class="border" />
<!-- after -->
<UiPopoverMenu>
<template #header>
<UiPopoverMenuUser :user="user" avatar-class="border" />
</template>
</UiPopoverMenu>
Those four props stay declared and deprecated through 2.x, and are removed in
3.0. The close button used to need an opt-in; it now renders by default and is
turned off with :closable="false".
The popover menu renders as a native popover
In browsers that support both the Popover API and CSS anchor positioning,
UiPopoverMenu now renders its panel as a native popover="auto": it moves to
the top layer, anchors itself to its place in the navbar, and the browser
handles click-outside and Esc dismissal (the component emits hideDropdown
to keep isOpened in sync). Browsers missing either feature keep the previous
class-based rendering, so nothing changes for them.
The API is unchanged — isOpened, @hide-dropdown, and any click-outside
handler keep working in both branches. The one thing that can break is custom
CSS that repositioned the panel: in the native branch .popover-menu sits in
the top layer as position: fixed, so absolute-positioning overrides written
against the old rendering stop applying there. Either target the native branch
explicitly with .popover-menu[popover], or opt the component out with
:native-popover="false" to force the class-based rendering everywhere.
anchor-name and the indicator references the active
one through a custom property (anchor(var(--_tabs-indicator-anchor) left)),
which is what lets transition: inset animate it: transitions need a computed
value change, and a fixed left: anchor(left) computes identically before and
after, so it would snap instead. Up to 16 tabs animate (8 navigation bar items);
beyond that the indicator still positions, it just jumps.Chips split the container from the selected color
.btn-chip used to carry its selected look in one place. It is now two axes:
the container the chip rests in, and the fill it takes once selected.
<!-- container: .btn-chip is the outline one, .chip-elevated the raised one -->
<button class="btn btn-chip">Outline</button>
<button class="btn btn-chip chip-elevated">Elevated</button>
<!-- color only lands together with .chip-active -->
<button class="btn btn-chip chip-active">Primary fill</button>
<button class="btn btn-chip chip-secondary chip-active">Secondary fill</button>
<button class="btn btn-chip chip-neutral chip-active">Neutral fill</button>
A color class on its own does nothing — an unselected chip is always just its
container — and .chip-active with no color class takes the primary fill.
.chip-elevated keeps its shadow through selection.
Two things change for existing markup:
.a40and.a60are gone. They tinted the active chip for side sheets and modals. Use.chip-neutralfor the same low-emphasis fill, which now tracks--theme-surface-container-highon every surface..chip-activeno longer forces a pill radius. It keeps the chip's own--_btn-radius, so selected chips match their unselected siblings. Add.btn-roundedwhere you still want the pill.
Card surfaces are static and interaction is its own class
A card's surface and its response to the pointer used to be the same decision.
.card-elevated stepped its shadow on hover and press, and .card-selectable
reset the border every variant had just set, so an outline card lost its outline
the moment it became selectable.
The two axes are now separate: .card-outline, .card-pane and
.card-elevated only describe the surface, and .card-selectable or
.card-link carries every hover, press and focus state.
<!-- surface only: no hover, no press -->
<div class="card card-elevated">…</div>
<!-- interactive: the surface answers the pointer, the shadow steps up -->
<a class="card card-elevated card-link" href="…">…</a>
<div class="card card-elevated card-selectable">…</div>
Two things change for existing markup:
- A plain
.card-elevatedno longer reacts to hover. It shows--shadow-smand holds it. Add.card-selectablefor a card the user picks, or.card-link(to/hrefonUiCard) for one that navigates, to get thesm→md→lgescalation back. - A card carries one surface class. They all set
--_card-bg-coloror--_card-shadow, socard-outline card-panewas only ever the last one to win. Drop the extra class rather than relying on the order.