Repository navigation
material 3.0.0
material 3.0.0 is the first stable release of the library published as mtrl 0.10.x.
The new package name is material. For upgrade steps, see
Migrating from 0.10.x in the changelog.
Security
- A password or hidden field's value is no longer painted before it upgrades (#53).
material3.0.0-next.0 andmtrl0.10.0 to 0.10.7 show a password or a hidden field's value in
clear text before the element upgrades; upgrading fixes it.
Migrating from 0.10.x
Upgrade to the latest mtrl 0.10.x first, then change the package name to material (version 3) and
follow this guide. The latest 0.10.x exports the 3.0.0 names beside the old ones, and marks
deprecated the TypeScript names, options and constants that 3.0.0 removes, so your editor flags
each use with its replacement. Two of those warnings can't be cleared before you upgrade, because
the new name exists only in 3.0.0: SELECT_CLASSES.TEXTFIELD (its new key is TEXT_FIELD) and
select.textfield (3.0.0 has select.textField). Clear the others, then upgrade. Each change's full entry follows this guide.
Packages and imports
- ESM only.
require('material')throwsERR_PACKAGE_PATH_NOT_EXPORTED. Useimport, or
await import('material')from CommonJS. - The root keeps the components. The composition core, the DOM, timing and store helpers and
the progress canvas code are imported from their subpaths, under the same names
(import { pipe } from 'material/core/compose'). The
migration table
gives every one. material/core/<area>only. A path under an area no longer resolves:
mtrl/core/compose/featuresbecomesmaterial/core/compose, which exports the same names
(withLifecycleamong them).- Folders inside a component no longer resolve (
mtrl/components/chips/chip,
…/features): they held internals, with no replacement. - Vue 3.4.20 or newer. With
skipLibCheck: false,@types/react18.2.71 or newer.
Renamed
| 0.10 | material 3.0.0 |
|---|---|
createTextfield, TextfieldConfig, TextfieldComponent, TextfieldDensity, TextfieldEvents, TextfieldValuePayload, TextfieldFocusPayload, TextfieldTrailingPayload |
createTextField, TextFieldConfig, TextFieldComponent, TextFieldDensity, TextFieldEvents, TextFieldValuePayload, TextFieldFocusPayload, TextFieldTrailingPayload |
TEXTFIELD_VARIANTS, TEXTFIELD_STATES, TEXTFIELD_TYPES, TEXTFIELD_EVENTS, TEXTFIELD_DENSITY, TEXTFIELD_DEFAULTS, TEXTFIELD_CLASSES |
TEXT_FIELD_VARIANTS, TEXT_FIELD_STATES, TEXT_FIELD_TYPES, TEXT_FIELD_EVENTS, TEXT_FIELD_DENSITY, TEXT_FIELD_DEFAULTS, TEXT_FIELD_CLASSES |
textfieldElement, defineTextfield, TextfieldSpec, TextfieldElement, TextfieldElementComponent (mtrl/elements) |
textFieldElement, defineTextField, TextFieldSpec, TextFieldElement, TextFieldElementComponent |
Textfield (mtrl/react, mtrl/solid, mtrl/svelte), MTextfield (mtrl/vue) |
TextField, MTextField |
Sass $textfield, textfield() |
$text-field, text-field() |
SELECT_CLASSES.TEXTFIELD |
SELECT_CLASSES.TEXT_FIELD, its value is select__text-field. A recorded exception: 0.10.x flags the old key but has no new one, so change it when you upgrade. In material 3.0.0 the old key reads undefined. |
select.textfield |
select.textField. A recorded exception, like the key above: 0.10.x flags the old name but has no textField, so rename it when you upgrade. In material 3.0.0 select.textfield reads undefined. |
<m-textfield> |
<m-text-field>. The old tag is not defined: it renders nothing. |
the classes mtrl-textfield, mtrl-textfield__…, mtrl-textfield--… |
mtrl-text-field, mtrl-text-field__…, mtrl-text-field--… |
::part(textfield) on the text field and on <m-select> |
::part(text-field) |
mtrl/components/textfield, mtrl/components/textfield/constants, mtrl/styles/textfield, mtrl/elements/css/textfield |
the same with text-field. 0.10.7 resolves both spellings. |
setComponentDefaults('textfield', …), setGlobalDefaults({ textfield: … }) |
the key 'text-field' |
CardSchema |
CardConfig |
the time picker's config option isOpen, and its default TIMEPICKER_DEFAULTS.IS_OPEN |
open and TIMEPICKER_DEFAULTS.OPEN, as on the dialog and the drawer. isOpen() is the method that reads the state |
TopAppBar, BottomAppBar (the factory types) |
TopAppBarComponent, BottomAppBarComponent |
a chip's text |
label |
tabs indicatorHeight, indicatorWidthStrategy |
indicator.height, indicator.widthStrategy |
shapes 'cookie4', 'cookie9' |
'cookie4Sided', 'cookie9Sided' |
createElement({ rawClass }) |
class or className |
the tooltip's rich |
variant: 'rich' |
onToggle on the icon button (mtrl/react, mtrl/vue, mtrl/solid; ontoggle in mtrl/svelte) |
onChange (onchange), whose event.detail is { selected, value }. A leftover is a type error: … is not assignable to type '"onToggle was removed in material 3.0.0: use onChange"'. |
Removed, with what to use instead
| 0.10 | material 3.0.0 |
|---|---|
the icon button's DOM toggle event |
change, on the factory's button (button.on('change', …)) and on <m-icon-button> |
segmented buttons (createSegmentedButton, createSegment) |
createButtonGroup({ kind: 'connected' }): see its entry for the option mapping |
the themes material, winter, browngreen, legacy |
baseline, ocean, brownbeige; legacy has none |
FAB and extended FAB variant: 'surface', FAB_VARIANTS.SURFACE, EXTENDED_FAB_VARIANTS.SURFACE |
a container or tone style ('primary-container', 'primary', …) |
FAB size: 'small', FAB_SIZES.SMALL, FAB_CLASSES.SMALL, FAB_ICON_SIZES.SMALL |
'default', 'medium' or 'large' (the extended FAB keeps small) |
getThemeColor('sys-color-X-rgb') |
getThemeColor('sys-color-X', { alpha }) |
TIMEPICKER_CLASSES |
TIMEPICKER_SELECTORS, which is not a like-for-like swap: see its row |
TABS_DEFAULTS.INDICATOR_HEIGHT, INDICATOR_ANIMATION_DURATION |
indicator.height, indicator.animationDuration |
DEFAULT_DATE_FORMAT from mtrl/components/datepicker |
the same from material/components/datepicker/constants |
the shape steps extra-tiny, tiny, pill |
literal 1px, 2px; full for pill |
Sass $mtrl-sys-shape |
v.shape(<step>) |
select.menu (the menu inside a select) |
the select's own open(), close(), isOpen(), getOptions(), setOptions() and its open, close and change events. The menu config option (container, maxHeight, …) stays |
splitButton.menu (the menu inside a split button) |
expand(), collapse(), isExpanded(), the expand, collapse and select events, and the new setItems() and getItems() |
progress.canvas, progress.track, progress.indicator, progress.buffer (the element), progress.resize() |
for the canvas, progress.element.querySelector('canvas'); nothing for resize(), the component observes its own size. setBuffer() and getBuffer() are unchanged |
Removed with nothing in their place, because nothing read them or they had no effect: a dialog
button's color, TOOLTIP_DEFAULTS.RICH, the checkbox's variant, CheckboxVariant and
CHECKBOX_VARIANTS, the list's prefix, the radios' rippleConfig and RADIO_VARIANTS,
RADIO_LABEL_POSITIONS, RADIO_SIZES, RADIO_CLASSES, RADIO_DEFAULTS.VARIANT,
.LABEL_POSITION and .SIZE, tabs' maxVisibleTabs, the time
picker's closeOnSelect and TIMEPICKER_DEFAULTS.CLOSE_ON_SELECT, TIMEPICKER_DIAL,
TIMEPICKER_Z_INDEX, TIMEPICKER_SELECTORS.MODAL, DIAL_CANVAS and DIAL_HAND, six
SLIDER_MEASUREMENTS keys, TABS_DEFAULTS.INDICATOR_ANIMATION_TIMING and ICON_SIZE,
TEXT_FIELD_CLASSES.LABEL_FLOATING, rippleConfig.timing and .opacity with their defaults,
the card, tabs and switch internals on their subpaths, ChipConfig's managedSelection and
cell, CardComponent's loading, expandable and swipeable, and the list scroll payload's
component, which was never sent.
Reserved before 3.0.0. mtrl 0.10.7 marks deprecated, so your editor flags each use,
ProgressComponent's canvas, resize, track, indicator and buffer (the table above).
Nothing marks the other three, so search for them: the slider's components, which is
internal in 3.0.0 and was never on the public SliderComponent type (TypeScript already
rejects it; use the slider's own API and slider.element); an import from
mtrl/core/compose/features, whose names are imported from material/core/compose; and a
read of tab.badge without a guard, since it may be undefined until the badge is shown
(a strict TypeScript project is already told; setBadge(), getBadge(), showBadge() and
hideBadge() work either way).
Sass. The two Sass rows above are for stylesheets that @use material's sources. The Sass sources
ship for reference; configuring them with @use … with is not a supported API in material 3.0.0. Theme
with CSS custom properties.
Type changes the compiler reports. Besides renames and removals, seven entries below change
a type your code may rely on: tabs' on and off take a closed event map, and a tab's click
payload is wrapped; React's and Solid's Button type their own onChange, so a
spread of full HTMLAttributes must omit it; SelectChangeEvent["value"] is
string | null; the chip set's change listener takes one object, not an array
and a second argument; a standalone chip's onChange and onClick take their
event's payload; and emit on the card and the tabs takes only the
component's own events, with their payloads. A config on* option is its event's listener
type: onConfirm: (time: string) => void, search onInput / onSubmit: (value: string) => void, and onSuggestionSelect: (suggestion: SearchSuggestion) => void are errors. A () => void callback is still assignable, so search
onClear, onExpand and onCollapse, and the navigation rail's onExpand
and onCollapse, are not. The search's expand and collapse are typed with the
object they emit, SearchStateEvent ({ component, state, viewMode }), in on(), off(),
the on map and onExpand / onCollapse: they were typed SearchEvent, so
event.value and event.preventDefault() there are now errors (they were undefined
and a TypeError at run time, measured), event.state and event.viewMode compile, and
a listener annotated (event: SearchEvent) => void on those two is an error. The
<m-search> component's on and off take the same map with the element's names
(open and close carry the SearchStateEvent), so a name outside it is an error too. The
<m-timepicker> component's on and off take the time picker's event map
(TimePickerEvents) in place of any string and an untyped handler: a name outside
it is an error, and each handler's argument is typed, so one annotated with another type, or
an argument on open, close or cancel, is an error. The time picker's isOpen is a
method: picker.isOpen === true and assigning it to a boolean are errors.
SnackbarState gains "queued", so a switch over it that had to be exhaustive is not.
The menu's and the select's open and close payloads (MenuEvent, SelectEvent) and the
select's change payload (SelectChangeEvent) have no preventDefault and no
defaultPrevented: none of these events could ever be cancelled, so
event.preventDefault() in such a listener is an error. The menu's select, where it keeps
the menu open, keeps both. ProgressComponent has no canvas, resize, track, indicator
or buffer: reading one is an error (TS2339). An import from a path under a core area,
material/core/compose/features, is an error too (TS2307).
Changes your compiler won't catch
Check these by searching your code: they compile, or come from plain JavaScript, markup or CSS.
-
An app with its own contrast switch adds
import 'material/styles/contrast'(and
material/themes/<name>-contrastfor a theme it imports on its own). Without that import,
data-theme-contrast="medium"or"high"changes no colour, and nothing warns.
Measured on the unthemed root: with the OS asking for more contrast,data-theme-contrast="high"
stays standard primary#6750a4, not high#312259. The OS preference (prefers-contrast: more)
still selects high contrast frommaterial/styles/base. Importmaterial/styles/contrastafter
material/styles/base, as withmaterial/styles/typography: the opt-in sheets share that cascade
layer. The contrast colours are the same in either order.data-theme-contrastis read
on the element that carriesdata-theme, or on the root when the page has nodata-theme;
on any other element it does nothing (that element inherits its themed ancestor's level). -
A select made with
createSelect()no longer fills its container. Unsized, it is
280px wide, as a text field is; it was as wide as what held it. Nothing warns: a form
whose selects spanned their column now shows them 280px wide. Search forcreateSelect(
and give each select that should fill its container a width
(.mtrl-select { width: 100%; }). -
A chip's
{ text }renders an empty chip, silently: no label, no error, no warning. -
Tabs
indicatorHeight/indicatorWidthStrategyare ignored: the indicator falls back to
its variant's height (3px on a primary row, 2px on a secondary one) and automatic width. -
materialShape('cookie4')throwsTypeError: byName[name] is not a function. -
createElement({ rawClass })applies no class and writes the value out as an attribute:
<div rawclass="legacy-a legacy-b">. On a component's config a leftoverrawClassdoes
nothing. -
A listener on the icon button's
toggleevent never fires: no error, and the button still
toggles. Listen tochange. In the React, Vue, Svelte and Solid components the compiler does
catch it:onToggle(Svelte:ontoggle) on the icon button is a type error. -
select.textfieldin JavaScript isundefined. -
The text field's classes, parts and defaults key are
text-field. A page rule on
.mtrl-textfield…or::part(textfield)matches nothing, aclassListcall with the old
class changes nothing, and defaults set under'textfield'are ignored. Nothing warns.
Search your CSS and your code fortextfield. -
A text field with a prefix or a suffix has no inline padding.
field.input.style.paddingLeftandpaddingRightread'', and the label has no inline
left: the stylesheet pads the input from--mtrl-text-field-prefix-widthand
--mtrl-text-field-suffix-width, which the field writes on its root. A page rule that set
the padding of.mtrl-text-field__inputbeside an affix was overridden by the inline value
and now competes with the stylesheet's rule. -
A FAB's
'surface'or'small'(the options take any string) renders as the default
primary-container, or at the default 56dp. -
data-theme="winter"(ormaterial,browngreen,legacy) on the root element gets the
baseline colours in the OS's colour scheme, anddata-theme-modeanddata-theme-contraston
that element are ignored. The OS contrast preference (prefers-contrast: more) is not applied
there either: a user who asked for more contrast gets standard contrast. On a nested element
it matches no rule, so that element keeps its ancestor's colours. -
getThemeColor('sys-color-primary-rgb')returns''(or thefallback), so
rgba(${…}, 0.12)yieldsrgba(, 0.12), a colour CSS and canvas drop silently. -
A removed constant key reads
undefinedin JavaScript, with no error:
SELECT_CLASSES.TEXTFIELD,FAB_SIZES.SMALL,TABS_DEFAULTS.INDICATOR_HEIGHTand the other
keys in the tables above. -
Your own CSS reading
var(--mtrl-sys-shape-corner-pill)(or-tiny,-extra-tiny): material's
stylesheet no longer declares the property, and avar()of an undeclared property without a
fallback gives no value, so the radius is lost silently. -
material/styles/baseno longer carries typography. Without
import 'material/styles/typography',.mtrl-display-large….mtrl-label-small,
.mtrl-text-center/left/right,.mtrl-font-thin/light/regular/
medium/bold, and.mtrl-truncate,-2and-3do nothing, andh1–h6andp
lose material's type styles. Measured in Chromium with only the base stylesheet: a
<div class="mtrl-headline-small">computedfont-size: 14px, inherited frombody
(with the import it is24px).
getPropertyValue('--mtrl-sys-typescale-title-large-font-size')returned"", and an
element styledfont-size: var(--mtrl-sys-typescale-title-large-font-size)inside a parent
at32pxcomputed32px: the custom property is undefined, so the declaration is invalid
at computed-value time andfont-sizeinherits (with the import the property is22px
and the element computes22px). Body text keeps its font:bodystayed14px
Roboto, sans-serif.import 'material/styles'is unchanged. The typography sheet has to
load after the base (both styleh1–h6andp; loaded first, it loses its bottom
margins to the reset):import 'material/styles/typography'imports the base first itself, and
a page using<link>tags putsstyles/typography.cssafterstyles/base.css. -
A bundle that evaluates an element CSS module (
material/elements/css/…) in an earlier task thandefine…()(a lazy route, a deferred hydration) no longer reserves that element's box in between. On a framework SSR page that does not load thematerial/ssrbridge (Next.js, Nuxt, SvelteKit, SolidStart), the hosts arrive with no shadow root, and the reserved box between the server HTML and hydration is gone. Put<link rel="stylesheet" href="…/material/elements/preupgrade.css">in<head>, orimport 'material/elements/preupgrade.css'(one element:material/elements/preupgrade/<name>.css). The link also reserves the box from the first paint, before any script. With the bridge loaded, the hosts already have their shadow roots and nothing is lost. -
Tab and panel ids change for any value with a character outside
[A-Za-z0-9_-]
(a.b→tabx-g-a_2e_b); a hand-written panel with the old id is never linked. Build ids with
tabIdForandtabPanelIdFor. -
Checkbox and switch
change.valueis the boolean checked state; the HTML string token is
valueAttribute, also inevent.detail. -
The chip set's
changehands its factory listener andonChangeone plain object,
{ value, selected, changed }, not an array with the changed value as a second argument. In
a leftover handlerevent[0]andevent.lengthareundefined,[...event]and
event.includes(x)each throw aTypeError(the message is the engine's), and a second
parameter isundefined. Readselectedandchanged.onChangehears the same changes
ason("change"), includingselectByValue(values, true), withchanged: null.
selectByValue(values)andclearSelection()stay silent. -
A chip's config
on*options are the matching event's listener. A leftover
onChange(selected, chip)on a chip alone is called with one object:selectedis
{ selected, chip, value }(soselectedis always truthy, and the boolean is
selected.selected; deselecting a chip valued"old"passes
{ selected: false, chip, value: "old" }), and the second parameter isundefined.
A leftoveronClick(chip)is called with{ event, originalEvent, element }:
elementis the chip's root, and the value is not the chip (focusisundefined).
The set'sonChangenow hearsselectByValue(values, true). A chip in a set emits
its ownchangewhen it is clicked, and the item'sonChangereceives that payload.
A leftoveronChange(selected, chip)on that item is called with one object, as on a
chip alone:selectedis{ selected, chip, value }(measured on a filter chip valued
"old": selecting passes{ selected: true, chip, value: "old" }, deselecting passes
{ selected: false, chip, value: "old" }), and the second parameter isundefined.
setSelectedandselectByValuedo not emit the chip'schange. A single-select click
emitschangeon the clicked chip only; the chip it replaces is updated with
setSelected, which stays silent. -
A chip's
onClickandclicklisteners run before the chip toggles, alone or in a
set:chip.isSelected()inside them is the state before the click. A chip alone used to
toggle and emitchangefirst. Read the new state inonChangeor achangelistener,
which follows. -
A chip's
changeis emitted only when the selection changed. In a
selectionRequiredset, a click on the last selected chip is refused: it used to emit
changeon the chip and on the set, with the chip still selected, and call bothonChange.
It now emits none, the item'sonSelectis not called, and<m-chips>dispatches no
change;clickandonClickstill report the press. -
A chip's
removelisteners in a set run before the set removes the chip. The item's
onRemoveand achip.on("remove")listener find the chip still ingetChips()and on the
page; the set then destroys it and emits its ownremove. A listener added withonused
to run after the set had destroyed and unlisted the chip. -
The time picker's
isOpenis a method,isOpen(). A leftover
if (picker.isOpen)compiles in JavaScript and is always true:picker.isOpenis now a
function. Call it. -
The time picker's config option
isOpenisopen. TypeScript reports a
leftover in an object literal. In JavaScriptcreateTimePicker({ isOpen: true })is
ignored: the picker stays closed (measured).TIMEPICKER_DEFAULTS.IS_OPENis
TIMEPICKER_DEFAULTS.OPEN; a leftover readsundefined. -
A snackbar waiting behind another is
"queued", not"visible". Right after
show(),snackbar.state === "visible"is true only if nothing else was on screen; it was
true at once. Usesnackbar.isOpen(), or listen toopen, which is emitted together with
the state turning"visible". -
A snackbar hidden while it is queued emits no
closeand nodismiss. It
was never open. It used to emit both, and was then shown anyway at its turn, with
state"hidden"and no way to hide it (measured). It now leaves the queue. -
A docked date picker opened with
open()from a click outside it stays open. That
click used to count as a click outside and closed it in the same task (measured:open,
thenclose). The next click outside closes it. -
Chip-set
addandremovefactory handlers receive{ value, chip }and
{ value, chip, chipValue }, not the chip;<m-chips>'sremovedetailvalueis the
remaining selection, and the removed id ischipValue. -
The time picker's
inputcarries the committed time invaluewhile the picker is open;
the live draft isdraftValue. Factoryconfirmlisteners andonConfirmreceive{ value }.
A leftoveronConfirm: (time) => { input.value = time }stores"[object Object]"(measured). -
Search
onInputandonSubmitreceive theSearchEvent. A leftover
onSubmit: (query) => `${query}`is"[object Object]"(measured); readevent.value. -
Search
onClearreceives theSearchEvent. It was declared with no argument and was
called with the query string,""after a clear. A leftover template of the argument is
"[object Object]"and.lengthisundefined(measured). The query isevent.value,""
after a clear. -
Search
onSuggestionSelectreceives theSearchEvent.`${suggestion}`was already
"[object Object]";suggestion.textis nowundefined(measured). The text is
event.suggestion.text(measured"Apple"). -
Search
onExpandandonCollapse, and the navigation rail'sonExpandandonCollapse,
were called with no argument, so a template of it was"undefined". Each now receives the
event's object, and a template of that argument is"[object Object]"(measured). The rail's
object is{ expanded: true }or{ expanded: false }. Searchexpandandcollapseemit
{ component, state, viewMode }and are declared as(event: SearchStateEvent) => void,
which is whaton("expand")accepts; that emitted object is unchanged, soevent.valueand
event.preventDefaultare not on it. -
The button group's
onmap (click,focus,blur,change) was accepted and never
called. Those handlers now run, with the listener's argument. The type is unchanged, so this
is not a compile error. The segmented-button note thaton.changekeeps its meaning describes
this. -
Options whose argument was already the event's now run first, before a listener added with
on(): time pickeronChange,onInput,onOpen,onCloseandonCancel; navigation rail
and navigation baronSelect; draweronSelect,onOpenandonClose; text field
onTrailingClick; the chip set'sonChange, and a chip'sonRemoveandonTrailingClick.
Time pickeronChangeis also the same{ value }objectchangeemits,
not a second one. -
An empty selection is
null, not"", in the select and the radios, factory and element:
thechangepayload'svalue, and the radio factory'sgetValue(). A comparison with""
is never true. -
The carousel's
changecarriesvaluealone:event.index(andevent.detail.indexon
<m-carousel>) isundefined. Readvalue. -
A
withInputyou compose yourself (material/core/compose) works on the checked boolean,
like the checkbox and the switch:change.valueandgetValue()are booleans, and
a leftoversetValue('x')checks the input and leaves its string unchanged (setValue('')
unchecks it), with no error. The string isvalueAttributein the payload, and
getValueAttribute()/setValueAttribute(). Novalueevent is emitted. -
A trailing icon without
trailingIconLabelis hidden from screen readers and loses its
pointer cursor; a click listener you added to it is out of their reach. The label is the
factory's (trailingIconLabel, orsetTrailingIcon(html, label)); on<m-text-field>and in
the framework components a trailing icon is decorative. -
The button group's
select(value)with a value no button carries clears the selection,
with a warning in development and no event. -
<m-button>dispatcheschangefor a toggle button. -
A dialog's
openlistener added after callingopen()never runs:openis emitted
inside the call.dialog.open(); dialog.on('open', build)opens an empty dialog, with no
error. Add the listener beforeopen(), or listen toafteropen. -
A dialog's
openlistener runs before the surface is visible and before focus is in it:
one that measures the dialog or moves focus belongs onafteropen. -
An open dialog answers Escape as soon as
open()returns, except the key press that
opened it. It ignored Escape and the scrim for its first 10 ms. -
An open dialog prevents every Escape key press that reaches the window, even with
closeOnEscape: false: akeydownlistener of the page on the window that runs after it
seesevent.defaultPreventedtrue, and nothing else the browser does for Escape happens
while a dialog is open. A listener on the document, or inside the dialog, runs before it. -
dialog.close()on a closed dialog anddialog.open()on an open one emit nothing. Code
that counted oncloseorafterclosefrom aclose()called "to be sure" no longer hears
them. -
A menu's
closelistener runs insideclose(), 50 ms earlier than it did: the menu is
still in the document and still has its visible class. One that read the DOM expecting the
menu gone must wait for the fade (350 ms). The same for a select'sclose, a split
button'scollapsewhen the user dismisses its menu, and a FAB menu'sclosein its
menupresentation. -
A listener for
openorcloseon<m-menu>or<m-fab-menu>no longer hears an
opening or a closing made by setting or removing theopenattribute (or the property):
that is applied at once and dispatches nothing, as on<m-dialog>. Code that set the
attribute and waited for the event waits for ever; read the state on the next line, or call
show()/hide(), which dispatch. -
fabMenu.isOpen()right afteropen()is true in themenupresentation too, before
its surface exists. Anopenlistener that reads the menu's surface must wait for it; a
second click on the FAB before the surface has arrived now closes the menu. -
event.preventDefault()in a menu's or a select'sopenorcloselistener, or in a
select'schangelistener, throws aTypeErrorin JavaScript: the payload has no such
method any more. It never did anything there; remove the call. -
menu.close(); menu.open()reopens the menu. Theopen()was ignored for the 50 ms
the close took, so the menu ended closed. -
menu.isOpen()right afterclose()is false. Code that waited 50 ms for it no
longer needs to; code that relied on it still being true (two menus open at once for a
moment, a toggle read just after a dismissal) now sees the closed state. -
List styles: for a 0.10.x app, the default is the baseline list: square rows and
state layers in every state, with no gap. Neither variant gives the list container a
background or a radius. To opt into rounded rows with their own colour and a 2px gap,
setvariant: 'segmented'(<m-list variant="segmented">). This choice is also needed
by a 3.0.0-next.1 user who wants to keep rounded rows; its rounded list container is gone
in both variants. The four row-shape properties apply only tosegmented. -
Switch and text-button layouts changed after next.1. At
font-size: 24px; line-height: 2,
an upgraded unlabelled<m-switch>is 52 × 48px (was 52 × 56); with supporting text its
height is 56px (was 65). Default typography is unchanged. A text button with an icon now
reserves its upgraded insets before upgrade, removing shifts of 4px at default/XS/S,
24px at M, 72px at L and 104px at XL. Check neighbouring content in your layouts.
Changed (breaking)
-
Explicit contrast levels are opt-in.
material/styles/baseandmaterial/themes/<name>
keep standard contrast andprefers-contrast: more.data-theme-contrast="medium"and"high"
(material-color-utilities contrast 0.5 and 1.0; the values are unchanged) move to
material/styles/contrastandmaterial/themes/<name>-contrast. The full stylesheetmaterial/styles
still includes them. Without the new import the attribute changes no colour and nothing warns.
Importmaterial/styles/contrastaftermaterial/styles/base, as withmaterial/styles/typography:
the opt-in sheets share that cascade layer. The contrast colours are the same in either
order (an explicit level is more specific than the standard rule, and the preference rule
does not match once the attribute is set). -
Typography leaves
material/styles/base. The base no longer carries the type
classes (.mtrl-display-large….mtrl-label-small), the text utilities (.mtrl-text-*,
.mtrl-font-*,.mtrl-truncate*), material's styles forh1–h6andp, or the
--mtrl-sys-typescale-*tokens, except the three thebodyrule reads
(--mtrl-sys-typescale-body-medium-font,-font-sizeand-line-height).
import 'material/styles/typography'restores what left. The full stylesheetmaterial/styles
is unchanged. Migration: without the import, a<div class="mtrl-headline-small">
computedfont-size: 14px(inherited frombody;24pxwith the import), and
font-size: var(--mtrl-sys-typescale-title-large-font-size)computed the parent's32px
because the property is undefined and the declaration is invalid at computed-value time
(22pxwith the import). Body text keeps its font (14px,Roboto, sans-serif). -
Pre-upgrade rules leave the element CSS modules.
material/elements/css/<name>no longer applies:not(:defined)rules when the module is evaluated. The reserved box comes frommaterial/elements/preupgrade.cssormaterial/elements/preupgrade/<name>.css(the element's spec name), in<head>or as an import. A hostrenderElementor a bridge renders with a shadow root carriesdata-mtrl-ssr. The stylesheet's last rule, inmtrl.preupgrade, rolls that layer back for the attribute, on the host, its::beforeand::after, and its direct children that are not themselves elements waiting to upgrade, so the stylesheet does not style a host the server already rendered or those children. Without the stylesheet, an element has no reserved box until it is defined.Migration: a bundle that evaluates the element CSS module in an earlier task than
define…()(a lazy route, a deferred hydration), including a framework SSR page that does not load thematerial/ssrbridge, loadsmaterial/elements/preupgrade.cssin<head>. With another tag prefix, a page that set it withconfigure({ prefix })inlinespreupgradeStyles(prefix)frommaterial/elements/preupgrade; the element CSS modules no longer apply these rules. -
Snackbar, time picker and date picker follow the overlays' one open and close rule. When
open()orclose()(the snackbar'sshow()orhide()) returns, the
state getter has changed and the event has been emitted; opening an open one and closing a
closed one do nothing and emit nothing;isOpen()is a method on every overlay.- Snackbar:
stateis"queued"betweenshow()and its turn on screen, then
"visible", withopenemitted at that moment; it said"visible"while waiting. New
isOpen(), true only while visible;statestays.hide()on a queued snackbar gives up
its turn and emits nothing. A queued snackbar dropped by aqueueBehavior: 'replace'
snackbar or byclearSnackbars()is"hidden"and can be shown again; it used to stay
"visible"for good, andshow()on it did nothing. Afterdestroy()the state is
"hidden"(it was left as it was), and a queued snackbar that is destroyed is not shown
at its turn. - Time picker: the
isOpenproperty is the methodisOpen(), and the config option
that opens the picker at creation isopen, notisOpen(its default is
TIMEPICKER_DEFAULTS.OPEN, notIS_OPEN): one name was a config key and a method on the
same component. <m-snackbar>: itsopenproperty isfalsewhile the snackbar waits behind
another, andtruefrom itsopenevent; it wastrueas soon asshow()was called.- Date picker: new
isOpen(). The click that callsopen()no longer closes a docked
picker: the event that opened an overlay never dismisses it. - The tooltip is outside the rule, by design:
show()andhide()wait for their
delays (300 and 100 ms unless called withtrue) and emit no event; readisVisible().
- Snackbar:
-
material is ESM-only. The CommonJS bundle (
dist/index.cjs) and the root'srequire
condition are gone;mainis the ESM entry. Every subpath was already import-only, and with
the internals off the root the bundle would have been a partial API.require('material')no longer
resolves (ERR_PACKAGE_PATH_NOT_EXPORTED): useimport, orawait import('material')from CommonJS. -
The package root exports the components and the app-level helpers only. The 137
internal names 0.10.4 deprecated on the root are gone from it: the composition core (pipe,
createBase, thewith*features), the DOM, timing and store helpers, and the progress
indicator's canvas code. The root keeps the component factories and their types,
configureHTML,schemeToTokensandTHEME_ROLES, and the global defaults. Each removed name
is the same export at its subpath; the
migration table
gives every one:import { pipe, createBase, withEvents } from 'mtrl'; // 0.10 import { pipe, createBase, withEvents } from 'material/core/compose'; // material 3.0.0
The subpaths are ESM-only, as material 3.0.0 is. The root's export list is pinned
(bun run root-exports:check), so a name cannot join it unnoticed. -
The Vue peer dependency is
>=3.4.20. The Vue adapter's declarations
importDefineSetupFnComponent, which@vue/runtime-corefirst declared in 3.4.20.
On Vue below 3.4.20 a project withskipLibCheck: falsefails to compile them
(TS2724); withskipLibCheck: trueevery Vue component isany. Migration:
install Vue 3.4.20 or newer. -
With
skipLibCheck: false, use@types/react18.2.71 or later. Earlier versions import
scheduler/tracing, which the current@types/schedulerno longer declares, so they fail to
compile withskipLibCheck: false, with or without material. -
Only the canonical names. For every row below but the last two, 0.10.5 exported
both spellings, the old ones deprecated; material 3.0.0 has only the canonical ones. Every exported identifier writes "text field" as
two words; the strings follow: see 'Text field in two words in every string' below. The declarations are renamed too, so the
types read the same in an editor. Svelte's component file isTextField.svelte. Migration:0.10 material 3.0.0 Entry createTextfieldcreateTextFieldmaterial(material/components/text-fieldexportscreateTextFieldtoo)TextfieldConfig,TextfieldComponentTextFieldConfig,TextFieldComponentmaterial,material/components/text-fieldTextfieldDensity,TextfieldEventsTextFieldDensity,TextFieldEventsmaterial/components/text-fieldTextfieldValuePayload,TextfieldFocusPayload,TextfieldTrailingPayloadTextFieldValuePayload,TextFieldFocusPayload,TextFieldTrailingPayloadmaterial/components/text-fieldTEXTFIELD_VARIANTS,TEXTFIELD_STATES,TEXTFIELD_TYPES,TEXTFIELD_EVENTS,TEXTFIELD_DENSITY,TEXTFIELD_DEFAULTS,TEXTFIELD_CLASSESTEXT_FIELD_VARIANTS,TEXT_FIELD_STATES,TEXT_FIELD_TYPES,TEXT_FIELD_EVENTS,TEXT_FIELD_DENSITY,TEXT_FIELD_DEFAULTS,TEXT_FIELD_CLASSESmaterial/components/text-field/constantsCardSchemaCardConfigmaterial,material/components/cardTopAppBar(type)TopAppBarComponentmaterial,material/components/top-app-barBottomAppBar(type)BottomAppBarComponentmaterial,material/components/bottom-app-bartextfieldElement,defineTextfieldtextFieldElement,defineTextFieldmaterial/elementsTextfieldSpec,TextfieldElement,TextfieldElementComponentTextFieldSpec,TextFieldElement,TextFieldElementComponentmaterial/elementsTextfield(component)TextFieldmaterial/react,material/solid,material/svelteMTextfieldMTextFieldmaterial/vueSass $textfield,textfield()(abstract/variables)$text-field,v.text-field(), the same map (both names in 0.10.5); the built CSS is unchangedSELECT_CLASSES.TEXTFIELD(deprecated in 0.10.5)SELECT_CLASSES.TEXT_FIELD,"select__text-field": no overlap, the key is new in material 3.0.0. A recorded exception to the rule that 0.10.x carries the replacement: a class-name key users rarely type, where an alias on 0.10.x would cost the select's last bytes.select.textfield(deprecated in 0.10.5)select.textFieldthe select's property: no overlap, textFieldis new in material 3.0.0, and readingselect.textfieldin JavaScript now givesundefinedrather than an error. The same recorded exception as the row above.Each is a rename of the import; the values and types are the same. The React, Solid and
SvelteTopAppBarandBottomAppBarcomponents keep their names: only the factory's types
were renamed. -
Text field in two words in every string. The identifiers were renamed
(createTextField,TextFieldConfig,defineTextField); the strings now follow, so the name
is written one way everywhere. The tag is<m-text-field>. The classes aremtrl-text-field,
mtrl-text-field__input,mtrl-text-field--focusedand so on, on the text field and inside
the select. The CSS part is::part(text-field), as parts are named after the classes. The
subpaths arematerial/components/text-field,material/components/text-field/constants,
material/styles/text-field,material/elements/css/text-fieldand
material/elements/preupgrade/text-field.css. The global defaults key is'text-field', as
'navigation-rail'and'side-sheet'are. The values ofTEXT_FIELD_CLASSESchange with the
classes, andSELECT_CLASSES.TEXT_FIELDisselect__text-field. The registry key in
elementsistextField. Generated ids start withmtrl-text-field-. Nothing else changes:
the identifiers, the options, the events and the rendered structure are the same, and
type="text"is untouched. The old strings have no alias: the old tag is not defined, an old
subpath throwsERR_PACKAGE_PATH_NOT_EXPORTED, and an old class, part or defaults key is
silently ignored. Migration: replacem-textfieldwithm-text-fieldin markup and
selectors,mtrl-textfieldwithmtrl-text-fieldin CSS and class calls,part(textfield)
withpart(text-field), and/textfieldwith/text-fieldin imports (0.10.7 resolves the
new subpaths, so imports can move first). -
Time picker, select and radio events agree with their getters.
Event 0.10 payload material 3.0.0 payload Factory time picker input{ value: draft }{ value: committed, draftValue: draft }<m-timepicker>inputdetail{ value: draft }{ value: committedOrEmpty, draftValue: draft }Factory time picker confirmtime string { value: time }<m-timepicker>confirmdetailno event { value: time }Factory select changeon empty ID{ value: "", ... }{ value: null, ... }<m-select>changeon empty ID{ value: "" }{ value: null }<m-radios>changeon empty ID{ value: "" }{ value: null }Factory radios changeon empty ID{ value: "", ... }{ value: null, ... }Factory radios getValue()with nothing selected""nullWhile the time picker is open,
input.valueno longer moves with the dial:
it is the committed time. ReaddraftValuefor live edits. The factory's
onInputcallback also receives the new{ value, draftValue }object,
described by the newly exportedTimePickerInputEventtype.onConfirmreceives
{ value }as well: see "A configon*option is the listener registered at
creation."SelectChangeEvent["value"]is nowstring | null
for an empty option ID. The radio factory reportsnulltoo:getValue()returns
string | null, andRadiosChangePayload["value"]isstring | null. Handlenull
for an empty selection in the select and the radios, factory and element alike. The radio
factory'ssetValueacceptsnulltoo, so the getter and the setter round-trip:
setValue(null)clears the selection, silently.<m-radios>'svalue = null, which the
React, Solid, Vue and Sveltevalueprops drive, clears through it; in 0.10 that cleared
with a development warning (no option with value ""). -
Chip-set
addandremovereport the live selection. Factory
callbacks receive one object instead of a bare chip:Event 0.10 payload material 3.0.0 payload addchip{ value: string | string[] | null, chip }removechipbefore removal{ value: string | string[] | null, chip, chipValue: string | null }after removal<m-chips>removedetail{ value: removedId }{ value: remainingSelection, chipValue: removedId }valuematchesgetValue()inside the callback, including for selected chips.
Inside a factoryremovehandler,event.chipis already destroyed and out
of the set. Its getters still answer, butgetChips().indexOf(event.chip)is
-1and its element is disconnected.
The element continues to emit oneremoveand no separatechangefor user
removal; declaration edits remain silent. Migration: read the chip from
event.chipin factory handlers and the removed identifier from
event.chipValue(orevent.detail.chipValueon the element). Read the
remaining selection fromevent.valueorevent.detail.value. The exported
ChipsAddEventandChipsRemoveEventtypes describe the new factory payloads. -
Checkbox and switch
change.valueis boolean. Factory payloads
and custom-element details now carry{ checked, value, valueAttribute, nativeEvent };
valuematches the checked model, andvalueAttributeholds the HTML string token.
Native forms still submit that token only while checked; setters remain silent and
element/framework bindings remainchecked-based. A standalonewithInput
(material/core/compose) emits the same payload (0.10 sent{ checked, value: <the input's string>, nativeEvent }), and its methods match it:getValue()andsetValue()work on
the checked boolean, and the input's string isgetValueAttribute()/setValueAttribute(),
the names the checkbox and the switch use. Both setters are silent: thevalueevent
setValue(string)emitted in 0.10 is gone.
Migration: read checked state fromvalue(orchecked), and replace reads of the
old stringvaluewithvalueAttribute, includingevent.detailin adapters. -
List event types match native forwarding.
scrollcarries
{ event, element, originalEvent }; its nonexistentcomponentfield is removed.
Migration: useelementfor the event root, or retain your list reference. -
Toggle buttons, chips and the carousel report
valuewithchange. Every
model event carriesvaluein the typegetValue()returns, read at dispatch:Component changepayload, 0.10 → material 3.0.0button, icon button (toggle) { selected }→{ selected, value: string }(the button's value)selectable chips (filter and input) { selected, chip }→{ selected, chip, value: string | null }carousel { index }→{ value: number }:valueis the index, andindexis goneThe custom elements'
event.detailcarries the same fields, and<m-button>now dispatches
changefor a toggle button.ButtonChangePayloadandIconButtonChangePayloadare
exported. Migration: keep readingselectedfor the toggled state; in a carousel handler
readvaluewhere you readindex(event.index, orevent.detail.indexon
<m-carousel>, is nowundefined); code that builds these payloads (mocks, test doubles)
addsvalue. In React and
Solid,Buttonnow types its ownonChange(the element'schange): code that spreads a
fullReact.HTMLAttributes(or Solid'sJSX.HTMLAttributes) intoButtonmust omit
onChange.The icon button's DOM
toggleevent, deprecated in 0.10.0, is removed (below):change
carries{ selected, value }. -
The chips set's
changepayload is a plain object and has one argument.
chips.on("change", handler)listeners and the config'sonChangeare passed
{ value, selected, changed }.valuekeeps the set's single or multi value
shape,selectedkeeps the selected chip values, andchangedis the toggled
chip's value.on("change")listeners get every change, withchanged: null
for one made by a method (selectByValue(values, true)); the config's
onChangehears those method changes too.<m-chips>still emits
changewith{ value }detail. Migration:// 0.10: the event was an array, and changedValue was a second argument. chips.on("change", (selectedValues, changedValue) => { use(selectedValues[0], changedValue); }); // material 3.0.0: on("change") and onChange are each passed one object. chips.on("change", ({ selected, changed }) => { use(selected[0], changed); });
Leftover 0.10 handler expression material 3.0.0 runtime result on the built package event[0],event.lengthBoth are undefined.[...event]Throws a TypeError. The message is the engine's: in Bun (JavaScriptCore),Spread syntax requires ...iterable[Symbol.iterator] to be a function.event.includes("a")Throws a TypeError. In Bun (JavaScriptCore):event.includes is not a function. (In 'event.includes("a")', 'event.includes' is undefined).Second changedValueparameterundefinedin bothon("change")andonChange; each handler is passed exactly one argument. -
Tabs
onandofftake a closed event map. A group accepts
change(TabChangeEventData). A single tab acceptsclick(the button's
wrapped{ event, element, originalEvent }payload),focusandblur
(the nativeFocusEvent).TabsConfig.onaccepts that samechangehandler.
Migration, what stops compiling:tab.on("click", (event: MouseEvent) => …): the payload is the wrapped
{ event, element, originalEvent }, so readpayload.originalEvent.createTabs({ on: { … } })with a key other thanchange:TabsConfig.onno longer has an
index signature.tabs.on("custom", …)for an event name of your own needs a cast.onandoffreturn the concrete component (TabsComponent,TabComponent), notthis.
-
emiton the card and the tabs takes the component's own events.CardComponent.emitand
TabsComponent.emitaccepted any event name and any data. They now take the names in
CardEventsandTabsEvents, each with the payloadonandoffdeclare for it. They are
the only two public component types that declareemit. Migration, what stops compiling:card.emit?.('custom', data); // 0.10 tabs.emit?.('change', { value: 'two' }); // 0.10: a partial payload tabs.emit?.('change', { tab, value: 'two' }); // material 3.0.0: the event's whole payload
- An event name of your own is a type error. At run time nothing changed: the emitter is
the same, so untyped code that emits'custom'still reaches a listener registered for
it. Keep your own events on an emitter of your own. - A component you build with
withEventskeeps its openemit(event: string, data).
- An event name of your own is a type error. At run time nothing changed: the emitter is
-
Tab and panel ids are derived from the value with a safe encoding. A value of
[A-Za-z0-9_-]only keeps its ids,tab-<group>-<value>andtabpanel-<group>-<value>. Any
other value getstabx-<group>-<encoded>and
tabpanelx-<group>-<encoded>:_becomes__and every other character_<hex code point>_, so the ids hold no whitespace (an id reference such asaria-controlsis a
space-separated list) and two values never share one. The tab element carries its value as
data-value, and the conventional panel is found from it rather than parsed out of the id.
tabIdFor(groupId, value)andtabPanelIdFor(groupId, value)are exported from
material/components/tabs. Migration: values whose ids worked before change too, not only those
with a space, a newline or a quote:a.b→tabx-g-a_2e_b,/home→tabx-g-_2f_home,
user:1→tabx-g-user_3a_1,café→tabx-g-caf_e9_. A page that writes its own panels
with the conventional id, or labels them with the tab's id, for any value with a character
outside[A-Za-z0-9_-]must build those ids withtabPanelIdFor/tabIdFor; a hand-written
tabpanel-g-a.bis otherwise never linked. -
Menu items and search suggestions are present synchronously when their factories return,
including in server-rendered shadow DOM. Migration: DOM inspection no longer needs
a timer before reading initial items or suggestions. Menu positioning still waits for
attachment; opening, focus, lazy submenus and suggestion updates keep their existing behavior. -
Text field: a trailing icon without
trailingIconLabelis decorative. It is
hidden from screen readers (aria-hidden) and no longer shows a pointer cursor. An app that
built a clear or show-password control from that span with its own click listener lost it for
screen-reader users. Migration: an interactive trailing icon needstrailingIconLabel(or
setTrailingIcon(html, label)), which makes it a button and emitstrailing. The label is a
factory option:<m-text-field>and the React, Vue, Svelte and Solid components have no label
attribute or prop, so a trailing icon there is decorative. -
The dialog is open when
open()returns, and closed whenclose()returns.
The rule, for the dialog first and for every overlay by material 3.0.0: whenopen()orclose()
returns,isOpen()has changed and the event has been emitted (the cancellablebeforeopen
orbeforeclosefirst). The classes, the paint, the focus trap and the animation may follow.
A dialog created withlayer: "top"and<m-dialog>already worked this way, and change
in two points only, marked "both layers" below; this is the factory dialog withoutlayer, which emittedopenand turned
isOpen()true 10 ms after the call. Migration: add everyopenlistener before calling
open()(or pass it in the config'son), and move toafteropenwhat needs the dialog
visible or focus inside it.// 0.10: the listener was added in time, because `open` came 10 ms later dialog.open(); dialog.on('open', build); // material 3.0.0: never runs, so the dialog opens empty // material 3.0.0: add it before the call dialog.on('open', build); dialog.open();
openis emitted insideopen(). A listener added after the call does not hear it:
add it before, or listen toafteropen. It runs before the surface is visible and before
focus is trapped;afteropenis the "visible, and focus is in" event.isOpen()is the dialog's own state, true on the line afteropen(). It no longer
reads the--visibleclass, which is still added 10 ms later so the surface has a state to
animate from.afteropenandaftercloseare unchanged in timing and are never emitted inside the
call, even withanimationDuration: 0:afteropenwhen the dialog is visible and focus
is in it,afterclosewhen it is removed. A listener added right afteropen()still
hearsafteropen.- Repeat calls do nothing and emit nothing, in both layers.
close()on a closed dialog
emittedbeforeclose,closeandaftercloseeach time, and a secondopen()within
the 10 ms emittedbeforeopenandopenagain. - The later call wins, in both layers.
open()thenclose()at once ends closed and the surface is
never shown (it was shown 10 ms later, on a dialog that had emittedclose);close()
thenopen()at once ends open and stays in the document (the pending removal took it
out). The call that lost emits noafteropenorafterclose: a dialog closed before its
afteropenwas due never emits it, and one opened again before itsafterclosewas due
emits noafterclose. Both used to arrive late, about a dialog in the other state, in
the top layer as well. - An open dialog can be dismissed as soon as
open()returns: Escape and a click on the
scrim close it from then, not 10 ms later. One exception, the same for every overlay: the
event that opened it never dismisses it. A dialog opened from an Escapekeydownhandler
stays open through that key press, and the next Escape closes it. "The event that opened
it" is exactly the one whose dispatch had begun whenopen()ran: any other, in the same
task or the next, counts. This holds in both
layers: alayer: "top"dialog and<m-dialog>opened that way used to close at once,
on thecancelthe browser sends for that same key press. destroy()right afteropen()leaves nothing behind (see Fixed).- Escape is handled as a key press, in both layers. The dialog listens on the
window and prevents the key, so the browser sends alayer: "top"dialog nocancelfor
it.closeOnEscape: falseand abeforecloselistener that refuses now hold for any
number of presses (see Fixed). Only the topmost open dialog answers; a key that something
open inside it has used (a menu, a select) is left to it. An Escape that cancels an IME
composition is left to the IME: a default-layer dialog no longer closes on it (a
top-layer one is the browser's to decide, as before).<m-dialog>still dispatchescancelfor every Escape, and
preventDefault()on it still refuses. A page listener that saw the nativecancelon
the factory's<dialog>for Escape sees one the dialog sends itself; a close request that
is not a key press (a back gesture) still arrives as the browser's.
-
The menu is closed when
close()returns. The same rule as the dialog's, for
the menu in both layers and for the two components that hold one, the select and the split
button.open()already worked this way;close()set the state and emittedcloseon a
50 ms timer. Migration: move out of acloselistener anything that needs the menu gone
from the document (it leaves 350 ms later), and dropevent.preventDefault()fromopen
andcloselisteners and from the select'schangelisteners.menu.close(); menu.isOpen(); // 0.10: true for another 50 ms. material 3.0.0: false menu.open(); // 0.10: ignored, the menu ended closed. material 3.0.0: it reopens
closeis emitted insideclose(), andisOpen()is false on the next line. The
listener runs while the menu is still in the document with its visible class: the class
andaria-hiddenfollow 50 ms later, the removal 300 ms after that, as before.- Repeat calls do nothing and emit nothing. Two
close()calls within 50 ms emitted
closetwice outside the top layer. - The later call wins.
close()thenopen()at once reopens the menu, with one
closeand oneopen;open()thenclose()at once ends closed and the surface
is never shown. - One menu at a time, in the call: opening a menu closes the one that was open before
its ownopenis emitted. The other'scloseused to come 50 ms after. - An open menu can be dismissed as soon as
open()returns: a click outside and
Escape close it from then, not 20 ms later. The event that opened it never dismisses it:
a menu opened by code from a click or a key press on another element ignores that click
or key press, and the next one counts. - Select:
close(), Escape and a chosen option setisOpen()false, emitcloseand
setaria-expanded="false"in that call or event. - Split button: when the user dismisses the menu,
isExpanded()turns false and
collapseandchangeare emitted in that event, not 50 ms later;collapse()then
expand()at once ends expanded (the menu ignored the reopening and collapsed the
button again). - FAB menu,
menupresentation:close()setsisOpen()false and emitsclosein
the call. Itsopen()is unchanged here. MenuEventandSelectEvent, the payloads ofopenandclose, and
SelectChangeEventlosepreventDefaultanddefaultPrevented, in the types and on
the objects passed at run time: nothing read them, and none of these events can be
cancelled. A leftover call throws; it never did anything. The menu'sselectkeeps
both: preventing it keeps acloseOnSelectmenu open.
-
The FAB menu is open when
open()returns, andopenon<m-menu>and
<m-fab-menu>is applied at once. The last two parts of the overlays' rule.
Migration: read the state on the line after settingopenon either element instead of
waiting for its event, or callshow()/hide().- FAB menu,
menupresentation:open()setsisOpen()andaria-expandedand
emitsopenin the call. It used to do so after the menu's module had loaded: a
microtask later once loaded, a network round trip the first time. The surface is shown
when the module has arrived.close()before that ends closed, with oneopenand one
close, and nothing is shown; it used to be ignored, and the menu then opened. If the
module fails to load, the FAB menu closes again (closeis emitted). Thelist
presentation already worked this way. <m-menu>and<m-fab-menu>, theopenattribute and property: applied inside
the attribute callback, so the element is open, or closed, on the next line. Applied
that way it dispatches noopenand noclose, as on<m-dialog>, the drawer and
the sheets. It used to be applied a microtask later and to dispatch both.show(),
hide(),toggle(), the anchor and the user still dispatch one event per opening and
per closing.
- FAB menu,
-
The modal sheets and the modal drawer handle Escape as a key press.
The bottom sheet, the side sheet and the drawer, when modal, join the dialog on one stack,
in both layers. Migration: nothing, unless a page listener relied on what follows.- A refusal holds. With
layer: "top", a sheet withcloseOnEscape: falseand a
drawer withdismissible: falsewere closed by the browser on the third Escape (it forces
the thirdcancelrefused in a row; measured on the dialog, the same<dialog>path).
The key is now prevented, so nocancelis sent. - Only the topmost modal answers, and after whatever is open inside it has used the
key. A dialog opened above a sheet takes Escape first. - The event that opened it never dismisses it: a sheet or a drawer opened from an Escape
keydownhandler stays open through that key press. In the top layer it used to close at
once, on thecancelthe browser sends for that key press. - Outside the top layer the listener moves from the document to the window: a menu or a
select open inside the sheet or the drawer takes Escape first. Every Escape that reaches
the window while one is open is prevented, a refusing one included. <m-drawer>still refuses Escape withno-close-on-escape: the key press is sent on
as acancelevent on its<dialog>, where it listens.- A standard (not modal) sheet is unchanged: Escape closes it when pressed inside it.
- A refusal holds. With
-
The pickers and the full-screen search handle Escape as a key press, and the time picker
is open when its factory returns. The last modals to join the dialog's stack.
Migration: readtimePicker.isOpen()right after creating it withopen: trueinstead of
waiting a task.- Time picker, modal date picker, full-screen search: Escape is prevented and answered
by the topmost modal only, after whatever inside it has used the key, and never for the
key press that opened it. Opened from an Escapekeydownhandler, each used to close at
once on thecancelthe browser sends for that key press. A picker opened above a dialog
takes Escape first, as before. - Time picker,
open: true:isOpen()is true andopenhas been emitted (to
onOpen) whencreateTimePicker()returns; it used to open on a 0 ms timer. The surface
is still shown a task later, where the picker has been put by then: a modal<dialog>
must not be moved once it is shown.close()before that task leaves it closed, with
nothing shown. - Docked date picker: the click that opened it is the only one it ignores. It ignored
every click in the taskopen()ran in, so a second click delivered before a timer had
run was lost.
- Time picker, modal date picker, full-screen search: Escape is prevented and answered
-
A config
on*option is the listener registered at creation. It runs with the same
argument, the same number of times, as a listener passed toon(event)at that point, and it
runs before a listener added afterwards. Whether a method notifies is unchanged (a silent
setValue,setActive,clear()or button-groupselectstays silent). The bottom app bar's
onVisibilityChangeand the top app bar'sonScrollhave no matching event, so they stay
callbacks and are not in the table. A chip'sonSelect
has no matching event either, so it stays a callback. A chip option used to be called beside
the emit, with its own arguments. A chip set registersonChangebefore a handler supplied
inon, and before a listener added aftercreateChips. A chip'sonChangeandonClick
are registered first: a chip's config has noonmap. A chip in a set emits its ownchange
on a click, after the set has toggled it, and the item'sonChangereceives that payload
({ selected, chip, value }). The calls on that click run in this order: the item's
onClick, the set'sonChange, a setchangelistener added aftercreateChips, the
item'sonSelect, a chipclicklistener added after the chip was created, the item's
onChange, and a chipchangelistener added after the chip was created.setSelected
andselectByValuedo not emit the chip'schange. A single-select click emits it on the
clicked chip only.Component Option Old argument New argument Time picker onConfirmthe 24-hour string { value }Time picker onChange{ value }, a second object from the onechangeemittedthe same { value }objectchangeemitsTime picker onInputthe inputeventthe same object Time picker onOpen,onClose,onCancelno argument no argument Search onInput,onSubmitthe query string the SearchEventSearch onClearthe query string (declared with no argument; ""after a clear)the SearchEventSearch onSuggestionSelectthe suggestion the SearchEvent(suggestionholds it)Search onExpand,onCollapseno argument the SearchStateEvent:{ component, state, viewMode }Navigation rail onExpandno argument { expanded: true }Navigation rail onCollapseno argument { expanded: false }Navigation rail, navigation bar onSelectthe select event the same object Drawer onSelectthe select event the same object Drawer onOpen,onCloseno argument no argument Text field onTrailingClickthe trailing payload the same object Button group on.click,on.focus,on.blur,on.changeaccepted, never called the listener's argument Chips onChange{ value, selected, changed }, and only for a user's changeThe same object as on("change"). The set'sonChangenow hearsselectByValue(values, true), withchanged: null.selectByValue(values)andclearSelection()stay silent.Chip onChange(a chip alone)(selected, chip){ selected, chip, value }, thechangepayloadChip onChange(a chip in a set)(selected, chip), and the chip did not emitchange{ selected, chip, value }, the chip'schangepayload. A click emits thatchange.setSelectedandselectByValuedo not.Chip onClickthe chip { event, originalEvent, element }, theclickpayloadChip onRemovethe chip the chip Chip onTrailingClickthe chip the chip Chip onSelectthe chip the chip. No matching event. createTimePicker({ onConfirm: (time) => { input.value = time; } }); // 0.10 createTimePicker({ onConfirm: ({ value }) => { input.value = value; } }); // material 3.0.0 createSearch({ // 0.10 onSubmit: (query) => console.log(query), onSuggestionSelect: (suggestion) => console.log(suggestion.text), }); createSearch({ // material 3.0.0 onSubmit: (event) => console.log(event.value), onSuggestionSelect: (event) => console.log(event.suggestion?.text), });
A listener added with
on()used to run before the option, which was called afteremit.
The option is now first.component.off(event, theSameFunction)removes it: the option is
that function, not a wrapper. A navigation rail or bar listener that destroys the component
duringselect, or the rail duringexpand, used to skip the option. The option now runs
first, so it is called; listeners already taken for thatemitstill run.In JavaScript, a leftover template of an argument that is now an object is
"[object Object]"
(measured), including assigning the time picker'sonConfirmargument to an input'svalue.
SearchonClear's.lengthisundefined; the query isevent.value. Search
suggestion.textisundefined; the text isevent.suggestion.text. The navigation rail's
expand and collapse templates were"undefined"when the option was called with no argument.A leftover chip
onChange(selected, chip)receives one object, measured on a filter chip
valued"old":Call arguments.lengthFirst parameter Second parameter Selecting 1 { selected: true, chip, value: "old" }undefinedDeselecting 1 { selected: false, chip, value: "old" }undefinedA leftover
onChange(selected, chip)on a chip in a set gets the same object, measured
on a filter chip valued"old": selecting passes{ selected: true, chip, value: "old" }
and deselecting passes{ selected: false, chip, value: "old" },arguments.lengthis 1,
and the second parameter isundefined.The object is always truthy, so a leftover
if (selected)stays true when the chip is
deselected. The boolean isselected.selected. A leftoveronClick(chip)receives one
argument,{ event, originalEvent, element }, whereelementis the chip's root; it is
not the chip component (focusisundefined), and a second parameter isundefined.
onRemoveandonTrailingClickstill receive the chip.chips.off("change", onChange)
removes the set's option. A handler that callsselectByValue(x, true)for one fixed
value runs twice — the change that was not that value, then the move onto it — and then
stops, becauseselectByValueemits only when the selection changed. -
material/core/<area>is an explicit list: seven areas, and no path under them.
The export map listed./core/*, and a*in an exports pattern crosses slashes: besides
the areas it resolved a folder inside one,material/core/compose/features, which the
README's "Building your own components" importedwithLifecycle()from (in 0.10.x too).
The map now namesmaterial/coreand its seven areas:material/core/canvas,/compose,
/dom,/shapes,/state,/themeand/utils. Any other path undermaterial/core
throwsERR_PACKAGE_PATH_NOT_EXPORTED, and TypeScript reports the import (TS2307).
Migration:0.10 3.0 import { withLifecycle } from 'mtrl/core/compose/features'(and any other name from that path)import { withLifecycle } from 'material/core/compose': the same name, the same functionEvery name
material/core/compose/featuresexported is an export of
material/core/compose, exceptwithBadgeand the typesBadgeComponentand
BadgeConfigof that feature, which no document named: they are internal; for a badge,
usecreateBadgefrommaterial.LabelManager, the type ofLabelComponent'slabel,
was only in the nested path and is now exported frommaterial/core/compose. -
Progress:
canvas,resize,track,indicatorandbufferare no longer on
ProgressComponent. They named how the indicator is drawn (one canvas;track,
indicatorandbufferwere that same canvas under the names of an older SVG), which
tied the public type to one way of drawing it. The objects are unchanged at run time;
the type no longer promises them, and how the indicator is drawn may change in a later
release. Readingprogress.canvasin TypeScript is now an error (TS2339). Migration:
for the canvas,progress.element.querySelector('canvas'); nothing replacesresize(),
as the component observes its own size;setBuffer()andgetBuffer(), the buffer's
value, are unchanged.
Removed
-
select.menuandsplitButton.menu. The menu inside a select or a split button
is no longer a member of either: a public handle on an inner component is what would stop the
menu from being loaded on demand later. Both were marked@deprecatedin 0.10.6. A leftover
.menuis a compile error in TypeScript; in JavaScript it readsundefined(measured), so a
call through it throws aTypeError. Use the component's own methods and events (the
migration table lists them). Changing a split button's items after creation, which only
splitButton.menu.setItems()did, issplitButton.setItems(). The select has no public way
to change itsplacementafter creation;<m-select>'splacementattribute still does. -
The options 0.10 deprecated are removed, first those it promised to remove in material 3.0.0. Migration:
0.10 material 3.0.0 createElement({ rawClass })(mtrl/core/dom)classorclassName, unprefixed since 0.10.rawClasswas applied on 0.10.x. In material 3.0.0createElementapplies no class for it and, as for any option it does not know, writes it out as an attribute:<div rawclass="legacy-a legacy-b">(an array becomesrawclass="a,b"). The seven component configs that typed it (those extendingBaseComponentConfig) never applied it, and a leftover there still does nothing, so for them only the type changes.a dialog button's colornothing: it had no effect; M3's dialog actions are text buttons in the dialog's colours the tooltip's richoptionvariant: 'rich';richwas never readTOOLTIP_DEFAULTS.RICH(deprecated in 0.10.5)nothing: it was the default of the removed richoptioncheckbox variant, and its typeCheckboxVariant(deprecated in 0.10.5)nothing: M3 has one checkbox style list prefixnothing: the prefix is fixed at build time radios rippleConfignothing: never applied; the stylesheet draws the state layer tabs ResponsiveConfig.smallScreen.maxVisibleTabsnothing: it never had an effect; for more than four tabs, use a scrollable row time picker closeOnSelect,TIMEPICKER_DEFAULTS.CLOSE_ON_SELECTnothing: never applied; the picker is confirmed with OK, as M3 specifies SLIDER_MEASUREMENTS.TRACK_RADIUS,SMALL_TRACK_EXTERNAL_RADIUS,LARGE_TRACK_RADIUS_RATIO,HANDLE_GAP_PRESSED_REDUCTION,CENTER_GAP,EDGE_PADDINGnothing: not read; the stylesheet and getExternalTrackRadiusgive the geometryTABS_DEFAULTS.INDICATOR_HEIGHT,INDICATOR_ANIMATION_DURATION,INDICATOR_ANIMATION_TIMING,ICON_SIZEindicator.heightandindicator.animationDurationto override; nothing read theseTEXT_FIELD_CLASSES.LABEL_FLOATINGnothing: a floating label is the field's --populatedor--focusedstateTIMEPICKER_SELECTORS.MODAL,DIAL_CANVAS,DIAL_HANDnothing: they matched no element (the dialog's ::backdrop, the DOM dial,__dial-trackand__dial-handle)FAB and extended FAB variant: 'surface',FAB_VARIANTS.SURFACE,EXTENDED_FAB_VARIANTS.SURFACE(deprecated since 0.8)a container or tone style ( 'primary-container','primary', …). The--surfaceCSS is removed, so a leftover'surface'renders as the defaultprimary-container.FAB size: 'small',FAB_SIZES.SMALL(deprecated since 0.8);FAB_CLASSES.SMALL,FAB_ICON_SIZES.SMALL(deprecated in 0.10.5)'default','medium'or'large': M3 Expressive has no small FAB. The--smallCSS is removed, so a leftover'small'renders at the default 56dp. The extended FAB'ssmallsize stays.a chip's text(ChipConfig, including a chip set's items)label. A leftover{ text }now renders an empty chip, silently: no label and no error or warning. Search your chip configs and chip set items fortext:.tabs indicatorHeight,indicatorWidthStrategyindicator.height,indicator.widthStrategy(since 0.3.2). A leftover is ignored: the indicator falls back to its variant's height (3px on a primary row, 2px on a secondary one) and automatic width.shape names 'cookie4','cookie9'(materialShape, the shapes)'cookie4Sided','cookie9Sided', Compose's names (since 0.10.2).materialShape('cookie4')now throws (TypeError: byName[name] is not a function).rippleConfig.timingandrippleConfig.opacity(button, icon button, FAB, extended FAB, button group, radios, tabs, and the coreRippleConfig); their defaultsDEFAULT_RIPPLE_CONFIG.TIMING,.OPACITY(button, icon button) andBUTTON_GROUP_DEFAULTS.RIPPLE_TIMING,.RIPPLE_OPACITY(deprecated in 0.10.5)nothing: never applied; the stylesheet draws the wave's motion and opacity. durationstays. The core'sRIPPLE_CONFIG.timing,.opacity,RIPPLE_TIMINGandRIPPLE_SCHEMA, on no public entry, go with them. -
material 3.0.0 exports nothing deprecated. What 0.10.0 deprecated and 0.10.x already replaced (or
never used) is removed, and so is what was kept only for compatibility: material 3.0.0 has no deprecated
export, member or event. Migration:0.10 material 3.0.0 CHECKBOX_VARIANTS(mtrl/components/checkbox,/constants)nothing: M3 has one checkbox style, and varianthad no effectRADIO_VARIANTS,RADIO_LABEL_POSITIONS,RADIO_SIZES,RADIO_CLASSES(mtrl/components/radios,/constants)nothing: no component read them RADIO_DEFAULTS.VARIANT,.LABEL_POSITION,.SIZE(deprecated in 0.10.6)nothing: the radios have no such options, and nothing read the keys. RADIO_DEFAULTS.DIRECTIONstays. In JavaScript a removed key readsundefined.the icon button's DOM toggleevent, from the factory's button and from<m-icon-button>(deprecated in 0.10.0)change, which carries{ selected, value }. A leftovertogglelistener never fires, with no error. In the React, Vue, Svelte and Solid components the icon button refusesonToggle(Svelte:ontoggle), and the compiler's error says what to do:Type '() => void' is not assignable to type '"onToggle was removed in material 3.0.0: use onChange"'. Without that guard the name would fall through to the host's nativetogglehandler, compile, and never fire.TIMEPICKER_DIAL,TIMEPICKER_Z_INDEX(mtrl/components/timepicker,/constants)nothing: the dial is sized in CSS and the picker is a modal <dialog>in the top layerTIMEPICKER_CLASSESTIMEPICKER_SELECTORS(public since 0.9.0), which is not a like-for-like swap: its values are prefixed selectors (".mtrl-time-picker__dial") where the old were bare class names ("time-picker__dial"), and 13 of the 33 old keys have no selector of the same name (ROOT,OPEN, the sixDIALOG_*,DIAL_NUMBER_ACTIVE,PERIOD_ACTIVE,TOGGLE_TYPE,CANCEL,CONFIRM)getThemeColor('sys-color-X-rgb')(mtrl/core/utils): the'r, g, b'triplet, derivedgetThemeColor('sys-color-X', { alpha }). The-rgbname now returns''(or thefallback), as any undeclared variable does, sorgba(${getThemeColor('sys-color-primary-rgb')}, 0.12)now yieldsrgba(, 0.12), an invalid colour that CSS and canvas drop silently: a missing colour, not an error. UsegetThemeColor('sys-color-primary', { alpha: 0.12 }). A theme that declares its own-rgbproperties is unaffected. -
Component subpaths export the component only, and are listed one by one. The
internals 0.10.5 deprecated onmtrl/components/<name>are gone, with no replacement: card's
withAPI,withLoading,withExpandable,withSwipeable,withElevation(a no-op: the
variant sets the elevation) and the*Featuretypes; tabs'with*features,addScrollIndicators,createTabsState,
createTabIndicator,updateTabPanels,setupKeyboardNavigationand their config and
component types; switch'swithSupportingTextandSupportingTextComponent. Datepicker's
DEFAULT_DATE_FORMATleaves the component's index but stays public in its constants:
import { DEFAULT_DATE_FORMAT } from 'material/components/datepicker/constants'.ChipConfig
losesmanagedSelectionandcell, which only the chip set sets.CardComponent'sloading,
expandableandswipeablemembers are removed;createCardnever set them. Tabs'
ResponsiveConfigandTabIndicatorand datepicker'sCalendarAPI, which public members are
typed with, are now exported. The manifest's./components/*and./components/*/constants
patterns become one entry per component (and per component with constants), so the folders
inside a component no longer resolve. Each component's export list is pinned
(bun run component-exports:check).
Migration: these subpaths now throwERR_PACKAGE_PATH_NOT_EXPORTED. They held internals, with
no replacement; a single chip iscreateAssistChipand the other factories in
material/components/chips, andCHIP_CLASSES/CHIP_STATESare internal class and state names:
mtrl/components/bottom-sheet/features,carousel/features,chips/chip,
chips/chip/constants,chips/features,drawer/features,list/features,menu/features,
progress/features,search/features(the low-levelwithInput),side-sheet/features,
slider/features,textfield/features. Every othermaterial/components/<name>and
material/components/<name>/constantsthat existed in 0.10 still resolves, except
segmented-buttonandsegmented-button/constants, removed with segmented buttons (see below). -
Segmented buttons are removed.
createSegmentedButtonandcreateSegment
(deprecated since 0.8.0), their types,mtrl/components/segmented-button,
mtrl/styles/segmented-buttonand the--mtrl-segmented-button-*properties are gone. M3
replaced the segmented button with the connected button group. Migrate:createSegmentedButton({ mode: 'multi', density: 'compact', // 0.10 segments: [{ text: 'Day', value: 'day', checkmarkIcon }] }); createButtonGroup({ kind: 'connected', selection: 'multi', density: 'compact', // material 3.0.0 buttons: [{ text: 'Day', value: 'day', selectedIcon: checkmarkIcon }] });
modeisselection(single,multi, ornonefor plain actions);segmentsare
buttons;checkmarkIconisselectedIcon.density,disabled,ripple,
rippleConfigandon.changekeep their meaning.enableSegment(value)/disableSegment(value)becomeenableButton(index)/
disableButton(index), orgetButtonById(id)for one button.- The
changeevent carries the selection; it has nooldValue, so keep the previous value
if you need it. - The button group's
select(value)now clears the selection when no button carriesvalue,
with a warning in development and no event, as the segmented button and the other selection
components do.
On arequiredgroup, which cannot be emptied, it warns and leaves the selection as it was.
-
The deprecated themes
material,winter,browngreenandlegacyare removed. 0.10 deprecated them; their files,mtrl/themes/<name>entries and
their rules in the full stylesheet are gone. A leftoverdata-theme="winter"(or any of the
four) on the root element gets the baseline colours in the OS's colour scheme, and
data-theme-modeanddata-theme-contraston that element are ignored: an app with its own
dark toggle follows the OS until it renames the theme. The OS contrast preference
(prefers-contrast: more) is not applied there either, so a user who asked for more contrast
gets standard contrast. On a nested element a removed name matches no rule, so that element
keeps its ancestor's colours. Migration:material→baseline,
winter→ocean,browngreen→brownbeige;legacyhas no replacement (pick any theme,
or keep its colours as custom properties of your own). -
The shape scale is M3's and nothing else. The mtrl-only steps
extra-tiny(1px),
tiny(2px) andpill(100px) are removed from$shape, with their
--mtrl-sys-shape-corner-*properties on:root.v.shape('tiny')and the rest now stop the
build with an error naming the migration (as does any step not on the scale). Migrate:extra-tinyandtiny: write the radius as a literal (1px,2px).pill: usefull, or half the component's height when its corners animate (a 9999px
radius snaps when animated).- A theme setting
--mtrl-sys-shape-corner-pillcan drop it; nothing reads it.
-
$mtrl-sys-shapeis removed fromabstract/theme. Nothing read it. Use
v.shape(<step>).
Added
- The list's
variant:'standard'(the default) or'segmented'.createList({ variant }),
with the typeListVariantand the constantLIST_VARIANTS(STANDARD,SEGMENTED) from
material/components/list/constantsandmaterial/components/list; an unknown value is the
default.<m-list variant="segmented">is the attribute, reflected by thevariantproperty,
and the React, Vue, Svelte and Solid components take the samevariantprop. The list's root
carriesmtrl-list--standardormtrl-list--segmented. Two new custom properties:
--mtrl-list-segmented-gap(2 px, the space between a segmented list's rows) and
--mtrl-list-item-container-color, the unselected row's colour in both variants (surface
by default in standard,surface-containerin segmented); set it totransparentfor rows
without their own colour. Like the four shape properties, the list reads them and declares
none, so set them on the list, on any ancestor, or on the<m-list>element. isOpen()on the snackbar and the date picker, as on every other overlay.- Split button
setItems(items)andgetItems().setItemsreplaces the menu's
items and returns the split button;getItemsreturns them. A split button created without
itemshas no menu andgetItemsreturns[]: the first non-emptysetItemscreates the
menu, which then works as one created with items (and opens at once if the split button is
expanded).setItems([])empties the menu and keeps it. Afterdestroy()it does nothing. - The navigation bar.
createNavigationBarand<m-navigation-bar>(with
<m-navigation-bar-item>), and the React, Vue, Svelte and Solid components: M3 Expressive's bar
for compact and medium windows, three to five destinations, from Compose'sShortNavigationBar.- Geometry and colour: 64dp on surface container; a 56×32 indicator on secondary container,
the active icon on it, the active label in secondary. itemLayout:'auto'puts the icon above the label, and beside it in a 40dp pill with the
items centred once the bar itself is 600px wide (a container query, so a bar in a narrow pane
stays vertical).'vertical'and'horizontal'fix it.- Destinations: links (
href) or buttons, badges (a count or the dot) folded into the
accessible name,aria-current="page"on the active one, in a namednavlandmark. Every
destination is a tab stop and the arrow keys move along the bar, as in the navigation rail,
whose destination code the bar now shares. hideOnScroll: off by default; it slides the bar away while the page scrolls down,
without the slide under reduced motion, and focus inside always brings it back.
- Geometry and colour: 64dp on surface container; a 56×32 indicator on secondary container,
- Every public
variantoption's type is exported frommaterialand from its component's
subpath. New on both:TextFieldVariant,SelectVariant,MenuVariant,ProgressVariant,
TabsVariant,TooltipVariant. New onmaterial(already on the subpath):BadgeVariant,
CardVariant,CarouselVariant,DatePickerVariant,ExtendedFabVariant,FabVariant,
IconButtonVariant,SearchVariant,SplitButtonVariant,ToolbarVariant. - Framework components accept the host element's HTML attributes.
React, Vue, Svelte and Solid props take that framework'sHTMLAttributesas well
as the component's own props. Where a name is both, the component's type wins, so
a switch'scheckedstays a boolean and a button'stypestays a string. The same
rule covers events. A component'schange,inputorselecthandler stays
the element'sCustomEvent, including in Vue, where those handlers come from emits and
would otherwise be intersected with the host'sonChange/onInput/onSelect. A global a supported release leaves off that interface is accepted too
(popoveron React 18 through 18.3.31 and on Vue through 3.5,enterKeyHinton React 18
before 18.3.31,nonceon React 18.0.0, Vue and Svelte);
a release that already declares the key keeps its own type. Vue spellsinputmode
anditemprop, and Solid and Svelte spellenterkeyhint. - Search listener types on
material/components/search, besideSearchEvent:SearchEvents
(each event's listener, soSearchEvents["expand"]types a handler) andSearchStateEvent
(whatexpandandcollapsecarry).material/elementsaddsSearchElementEvents, the same
map with<m-search>'s names (change,select,open,close). - Read-only
getValue()aliases on carousel, tabs, drawer, navigation rail and
button group; existing accessors remain. Button togglechange, card
expandedChanged, listkeydown, and interactive touch events now have their
actual payload types, including both slider touch delivery shapes. material/ssr:renderElementrenders elements as declarative shadow DOM on Node or Bun; server-only, with no runtime dependencies.<m-toolbar>renders a declarative shadow root like the other elements. Before upgrade, each toolbar item is its own tab stop; after upgrade, the toolbar is one. Carousel and FAB menu stay opted out. It inlines the CSS by default, or links the stylesheets in the same order as the browser and the inline styles (without adding build-manifest dependencies), renders nested elements, and applies the shared HTML policy; each call defines only the host tags it meets, including nested authored and factory-generated elements, instead of recreating all 36 classes. Asynchronous FAB-menu and submenu configurations use the host-only fallback. The identity HTML policy is not a sanitizer; configure a synchronous sanitizer for untrusted markup. The React, Svelte, Solid and Vue bridges render a host that carries ordinary HTML attributes (popover,inputmode,enterkeyhint,itemprop,nonce,is, and the rest of the host's HTML attributes): the framework emits those attributes on the host, and the shared renderer skips names outside its allowlist, including event-handler names andsrcdoc, so they never enter the shadow markup. CallingrenderElementdirectly rejects an unknown host attribute. Underneath are an internal detached element lifecycle, style registry seams, and a synchronous server DOM scope with inert scheduling and complete resource teardown. Worker and edge runtimes are unsupported inmaterial3.0.0. A resolver that triesworkerdorworkerbeforebrowser(Cloudflare Workers) loads the browser stub:renderElementthrows "material/ssr is server-only", and importingmaterial/ssr/react,material/ssr/vue,material/ssr/svelteormaterial/ssr/soliddoes nothing, so the page has no declarative roots and no error.material/ssr/react: an opt-in, server-only entry. Imported in the server bootstrap, it makes the React adapters emit styled declarative shadow roots during SSR (React 18 and 19), with no server code in the client bundle. Without it, React output is unchanged. ASuspenseboundary inside a component contributes its fallback to the server-rendered shadow root: a button has no label slot with an empty fallback but has one with a text fallback; a boundary around a tab leaves the root without that tab with either fallback. Put the boundary outside the component when the server root needs resolved content. A child that suspends stays on the server: the static pass retries the host only when that render suspended, and the shadow root is built from the children once they can render. A child that throws is not retried: the page render reaches it, so the error is reported as it is without the bridge. When that separate render throws something other than a suspension, development logs one warning per host in the response, naming the element and the error; production logs nothing. Retries wait on a backoff (doubling to 250ms) and stop after 40 attempts, about eight seconds; past that the host has no shadow root and the page keeps streaming, so a slower child still arrives. The suspension is recognised from the throw site of whichever React build this process loaded, not from the error text, so a production build that minifies the message still retries. At the cap, development logs one warning naming the element; production logs nothing. When no stack frame can be read, development logs one warning that suspending children render without a server shadow root; production logs nothing. The server-rendered shadow root is built in a separate render, without the context of providers above the component. The page's own render (the light DOM) sees the provided value. Until upgrade, a child reading context with a default shows that default in the painted shadow root; a child requiring its context leaves this component without a declarative shadow root while the page still renders. Pass the resolved string as a prop or attribute, or accept client-rendered text until upgrade. A fix is planned for a later 3.x release. The Vue and Solid bridges see the provided value in both the shadow root and light DOM.material/ssr/svelte: an opt-in, server-only entry. Imported in the server bootstrap, it makes every generated Svelte component emit a styled declarative shadow root during SSR, on the sameSymbol.for("mtrl.ssr")bridge as React, with no server code in the client bundle. Carousel and FAB menu emit no template. Without the import, Svelte output gains only the empty branch marker. Like React, its shadow root is built in a separate render without provider context; the page's light DOM sees the provided value. A child reading context with a default shows that default in the painted shadow root until upgrade. A child requiring context leaves that component without a declarative shadow root while the page still renders: the host falls back to light DOM, then upgrades normally in the browser. Development logs once per affected host in each response, naming the element and including the child render error; production logs nothing.material/ssr/vue: an opt-in, server-only entry. Imported in the server bootstrap, it makes every generated Vue component emit a styled declarative shadow root during SSR, on the sameSymbol.for("mtrl.ssr")bridge as React, with no server code in the client bundle. Carousel and FAB menu emit no template. Without the import, Vue output is unchanged. A host's child may useasync setup()underSuspense, including data created outside that child andrenderToWebStream: the shadow bridge serializes those children once. A host whosev-htmlcontains an unclosed<template>renders, andmaterial/ssr/vueimports the server renderer fromvue/server-renderer.material/ssr/solid: an opt-in, server-only entry. Imported in the server bootstrap, it makes every generated Solid component emit a styled declarative shadow root during SSR, on the sameSymbol.for("mtrl.ssr")bridge as React, with no server code in the client bundle. Carousel and FAB menu emit no template. Without the import, Solid output is unchanged. Async and streaming SSR finish when a component inside a host creates a resource under an outerSuspense: the shadow bridge reuses the page's serialized children, preserving its resource ownership and hydration keys without rendering children twice.- Per-element SSR opt-out: specs accept
ssr: falseor a synchronous host
predicate. Carousel and FAB menu emit their host and light DOM without a
declarative root; menu and split-button do the same for nested submenus.<m-toolbar>
renders a declarative shadow root. Async
button/card global defaults conservatively use this fallback for the whole render.
Eligible light-DOM descendants still render their own roots. Pre-upgrade CSS keeps
the host's box until browser upgrade, and these paths no longer throw. React SSR
honors the same opt-out without emitting an empty declarative template. - Element CSS also ships as
.cssfiles (material/elements/css/<name>.css,hosts/<element>.css), for server-rendered<link>styles. ssr:check(CI): server-rendered elements are checked in Chromium, Firefox and WebKit, for a styled first paint without JavaScript, pixel stability and no layout movement on upgrade, and the security reparse; markup parity stays in Chromium. Chromium security and per-node parity checks cover all 36 element defaults.ssr:checkandsvelte-ssr:checkcover two more cases: a toolbar's server-rendered
icon buttons are measured across the upgrade (pixels, layout, and each button keeping its
parser-created root), and Svelte named snippets (cardheadlineandactions, top app bar
leadingandtrailing) are checked as slotted before script and adopted by hydration.- Every
material/components/<name>entry exports its factory both ways: as the default
export and by its name (import createButton from 'material/components/button'and
import { createButton } from 'material/components/button'are the same function, and the
same one the root exports). 21 entries had only the default and gain the name;chipsand
dividerhad only the name and gain the default. Nothing is removed.
Changed
- Lists are the baseline list by default; the expressive style is
variant: 'segmented'.
A list has two styles, as on m3.material.io ("The standard and segmented styles are a visual
choice, and don't affect a list's behavior").standard, the default, has square rows and
square state layers in every state, and no gap.segmentedis the expressive list, as
Compose'sSegmentedListItem: each row paints its own colour (surface-container;
secondary-containerwhen selected) and its own shape (4 px where it meets another row,
16 px on the outer corners of the first and the last row, 12 px hovered, 16 px focused,
pressed or selected), with 2 px between rows. A divider or a subheader ends a group: the rows
beside it take their outer corners. A selected row is 16 px on every corner whatever its
place in the list. The four shape properties (--mtrl-list-item-shape,
--mtrl-list-item-shape-outer,--mtrl-list-item-shape-hover,
--mtrl-list-item-shape-active) apply to a segmented list; a standard list does not read them.
The rounded list container of 3.0.0-next.1 is gone, in both variants: the list has no
background and no radius of its own, so--mtrl-list-item-shape-outerrounds rows only, and
a list given a height scrolls in a square box. - Text field: the spacing follows the M3 measurements. A field's layout shifts
by the amounts below; nothing in the API changes. Sources: the measurement tables on
m3.material.io's text fields page, and Compose'sTextFieldImpl.ktfor the positions the
site gives only as diagrams.- A filled field's text is 24px down under its label and 8px above the bottom edge
("Top/bottom padding 8dp"; the label's 16dp line is between): it was 22px down and 10px
above. The floated label's line is centred 16px down, 1.8px lower than it was. A select's
text moves with it. Without a label the text is centred, 16px down
(contentPaddingWithoutLabel): it kept the labelled field's place, 22px. - An outlined field's text is 16px down (was 15.5px), and its floated label is centred
on the top edge (it sat 1.7px below it). - A prefix or suffix is on the text's line in every case; in a compact field it was
1.5px above it, in an outlined one half a pixel below. - Compact density, which M3 does not measure, keeps its own heights and takes the rules
that are not numbers: the outlined floated label centred on the edge (1.5px lower
before), the text centred in an outlined or unlabelled field (10px down; it was 10.5px
outlined, 13.5px filled without a label), and the insets around its 20px icon box, 12px
and 16px. - An icon is 12px in and what follows it 16px further ("Left/right padding with icons
12dp", "Padding between icons and text 16dp"): beside a leading icon the text and the
label start 52px in, and before a trailing icon the text ends 52px in. Both were 44px (the
outlined field's text 45px). At compact density, whose icon box is 20px, 48px (was 40px,
and 41px outlined). A select's text ends 52px before its end, as it has a trailing icon. - A filled field's floated label stays beside the leading icon, 52px in, where the
resting label is (Compose places both with one expression). It moved to 16px. The outlined
field's floated label goes to the notch, 16px in, as before. - A prefix starts where the content does, 16px in (
TextFieldPadding), or 52px beside a
leading icon; it was 12px, and 44px. An affix is 2px from the text
(PrefixSuffixTextPadding): the prefix was 4px from it (5px outlined), and the text
touched the suffix. - The label is not moved by a prefix (Compose: "Prefix/suffix does not get applied to
label"). Resting, it starts 16px in, where it was 25.5px beside a "$" (the prefix is hidden
while the label rests). Floated in a filled field, 16px, where it was 12px. - The outlined input has no border at its sides: its transparent border is top and
bottom only and its horizontal padding is 16px (was 15px beside a 1px border), so both
variants place the text at the same pixel. The outline itself is unchanged. - A multiline field's first line and label are where a single-line field has them
(Compose places the text with no single-line branch). The first line starts 24px down in a
filled field with a label (was 12px) and 16px down in an outlined field or without a label
(was 13px and 12px); the resting label is 16px down (was 12px), and the floated label is
the single-line field's (4px lower than it was). Compact, which M3 does not measure, takes
the compact single-line field's: the first line 14px down under a filled label (was 8px),
10px otherwise (was 8px and 9px). The Sass map'spadding-top-multilineis 16px (was
12px) andpadding-top-multiline-compact10px (was 8px), with two new keys for the filled
field's label,padding-top-multiline-label(24px) and
padding-top-multiline-compact-label(14px). The 100px minimum height stays: M3 gives a
text area no height ("Text areas are taller than text fields and wrap overflow text onto a
new line … These should be used instead of multi-line fields on the web",
m3.material.io/components/text-fields/guidelines, "Input text"); Compose's 56dp is its
multi-line field, which starts as one line and grows.
- A filled field's text is 24px down under its label and 8px above the bottom edge
- SSR docs: what the two style modes cost. Inline styles stay the default. The
README's server-rendering section andRenderOptions' TSDoc now say what inline costs (gzip
cannot see a repeat further back than its 32 KB window, so serve brotli or use link mode
for pages with many selects, or that mix large roots (text fields, dialogs) in turn; and
the HTML is 0.5 to 0.9 MB uncompressed for 30 to 44 roots) and link mode's caveat (WebKit paints the roots unstyled until the
stylesheets arrive). No code changes. - What
open()has done when it returns is documented and pinned by tests. The
surface may be painted afteropen()returns; the state is not deferred. On return: a
select'sisOpen()is true, its input hasaria-expanded="true"andopenhas been emitted;
a split button'sexpand()has setisExpanded()and emittedexpandandchange; a time
picker'sisOpenis true andopenhas been emitted; a date picker has emittedopen; a
dialog has runbeforeopen, and withlayer: "top"it is open and has emittedopen. None
of this changed in material 3.0.0. The first ArrowDown, ArrowUp, Enter, Space, Home, End or typed character on a closed
select opens it and is not lost. - SSR docs. The README names every attribute whose value is markup, including
avatarandleading-avatar, which are not a person's name or an image URL;FabMenuConfig.closeIconis markup too. The React and Svelte bridges build the server-rendered shadow root without the context of providers above the component. - CI's Solid and Vue SSR runs on the lowest supported peer version are ordinary commands,
solid-ssr:floorandvue-ssr:floor. Each reads the floor frompeerDependencies,
installs it without saving, runs the check and restores the installed version, so nothing after
it runs on the floor version unnoticed. - CI runs the same checks in less time. The browser checks run in five groups instead of
three, the package checks no longer hold the browser groups back, and Playwright's browsers
and their system packages come from a cache that every pull request can read (a slow Ubuntu
mirror made one install step take 26 minutes).test/build/ci-commands.test.tslists the
commands CI runs and fails when one is dropped. - An
<m-text-field type="hidden">takes no space, before upgrade or after. The upgraded
hidden field was an inline-block host taking a line box, with itsdisplay: noneinput inside;
it is now not rendered at all, as an element with thehiddenattribute is not. Its value still
submits with its form and still reads fromvalue. Every other type is unchanged. - An upgraded
<m-switch>is shorter under a tall line height than in the 3.0.0 prereleases.
At a page'sfont-size: 24px; line-height: 2the upgraded switch is 52 × 56 → 52 × 48
unlabelled, and 65 → 56 tall with supporting text, so in a flex row centred on it the text
beside rises 4–4.5 px and what follows 8–9 px. At the default type nothing moves. Text beside
an unlabelled<m-switch>no longer moves when the element upgrades. Its pre-upgrade
baseline now matches the control's center; a supporting-text-only switch uses its text
baseline. The upgraded switch host uses the control's box directly, so 12px or 24px
surrounding text with line-height 1 or 2 cannot add an extra host line box. Inline and
baseline/center flex layouts are checked in both text directions. - The package's README on npm is a short one.
npm-readme.mdis packed as the package's
README.md(install, one example, the component list, and links to md3.io); the full
README stays on GitHub. Nothing in the API changes. - The slider's label takes the Body Large role, and a labelled horizontal slider is 4px
taller. The label on a slider is this library's — the specification puts no label
on a slider — so the form controls' label role decides, as it does for the checkbox, radio
and switch labels: the label's text is now 16px on a 24px line (was 18px on an inherited 20px
line). A horizontal slider with a label is 4px taller: 72 → 76px at XS and S, 76 → 80 at M,
92 → 96 at L, 132 → 136 at XL; a vertical slider's height is unchanged. A layout that
reserved the old height gains 4px per labelled slider. - The element authoring API is experimental.
defineElement,ElementSpec,
registerStyles,hasStylesandSHADOW_BASE_STYLES(frommaterial/elements), the API
for writing custom elements of your own on material's machinery, are tagged
@experimentalin their TSDoc and are outside semantic versioning in 3.x: they may change
in a minor release. Nothing about them changes in this release. The elements material
defines (<m-button>,<m-text-field>and the rest), their attributes, properties and
events, and thedefine…()functions are covered by semantic versioning as before. If
you build your own elements on this API, pin the minor version (~3.0.0). - Slider: the
componentsbag is internal. The slider's controller reads an older
componentsobject as a fallback for its elements; nothing in the library fills it, the
publicSliderComponentnever had it, and it is now marked internal and outside the
public contract: it may go in any release. Use the slider's own API (setValue(),
getValue()and the rest ofSliderComponent) andslider.element. - Tabs:
tab.badgemay beundefineduntil the badge is shown. The type always allowed
it (badge?: BadgeComponent); it is now the documented contract, ontab.badgeand on
getBadgeComponent(): a tab creates its badge no later than when it shows it, so a later
release can create it only then. Nothing changes at run time in this release: the badge
still exists from the firstsetBadge(), or from creation with thebadgeoption. Use
setBadge(),getBadge(),showBadge()andhideBadge(), which work whether the badge
exists yet or not, and checktab.badgeforundefinedbefore reading it. - A short chip with a secondary action is wider, by the specification. Material 3, Chips:
"Secondary actions (such as a trailing icon button for Remove) must have a 48x48dp
interaction target that doesn't interfere with the chip's primary action (such as Edit or
Drag). To achieve this, apply a minimum width of 88dp to the chip, or 42dp to the label
text." A chip with a remove or trailing button is therefore at least 88px wide, its label
at least 42px: the last 48px of the chip are the secondary action's target, by the
specification, and the chip's own action owns the rest, at least 40px. An input chip
labelled "Label" is 88px (was 80.05) and one labelled "OK" 88px (was 64.5); a filter chip,
whose action starts 16px in, measures 92px (was 84.05 for "Label" and 68.5 for "OK"). A
chip whose label already filled the 88px floor is unchanged. - An extra-small button's space between its icon and its label is 4px. It was 8px. Material 3's token
md.comp.button.xsmall.icon-label-spaceis 4, and Compose'sButtonDefaults.ExtraSmallIconSpacingis 4. - An unsized select is 280px wide, as an unsized text field is.
createSelect()made a
select that took its container's whole width (200px in a 200px container, 400px in a
400px one), where the text field, on both paths, and<m-select>are 280px whatever holds
them. The factory's select now sizes as they do. Migration: to keep a select filling its
container, give it the width:select.element.style.width = '100%'or the rule
.mtrl-select { width: 100%; }; for the element,m-select { width: 100%; }. There is
no option for it.
The fixes (### Fixed, 46 entries) are not shown here: GitHub limits a release to 125,000 characters.
Full release notes: CHANGELOG.md
npm: material@3.0.0 · Docs: md3.io · Full history: CHANGELOG.md
The history before 3.0.0 was developed in floor/mtrl; #numbers in commit subjects before 3.0.0 refer to pull requests there.