Repository navigation
material 3.0.0-next.1
Pre-releaseThe second prerelease of 3.0.0, on the npm next tag (npm install material@next). Its full
entries are below; this is what changed since 3.0.0-next.0.
Breaking since next.0
material/core/<area>is an explicit list:material/coreand its seven areas (canvas,
compose,dom,shapes,state,theme,utils).material/core/compose/featuresno
longer resolves: import the same names frommaterial/core/compose.ProgressComponentno longer hascanvas,resize,track,indicatorandbuffer
(TS2339). For the canvas:progress.element.querySelector('canvas').
Layout changes since next.0: check your pages
- A select made with
createSelect()is 280px wide unless you size it; it filled its
container. To keep that:.mtrl-select { width: 100%; }. - A select's menu is as wide as its field in both layers (it stopped at 280px in the default
layer). - A chip with a remove or trailing button is at least 88px wide, its label at least 42px
("OK": 64.5 → 88px). - A labelled horizontal slider is 4px taller (72 → 76px at XS and S, 76 → 80 at M, 92 → 96
at L, 132 → 136 at XL); its label is 16px on a 24px line (was 18px). - An unlabelled switch is 52 x 48 (was 64 x 56).
- An unlabelled checkbox's box is centred in its 48px target (15px on each side; it sat at
the start). An unlabelled radio is centred the same way. - A radio row grows with a wrapping label (it was a fixed 48px, and the text overflowed it);
a one-line row stays 48px. - An extra-small button has 4px between its icon and its label (was 8px).
- Right to left: a small button's icon padding, the text field's trailing icon button's
target, a selected menu item's check mark and<m-text-field>are mirrored.
Also since next.0
- Every
material/components/<name>entry exports its factory as the default export and by
its name. - The element authoring API (
defineElement,ElementSpec,registerStyles,hasStyles,
SHADOW_BASE_STYLES) is experimental: outside semantic versioning in 3.x. tab.badgemay beundefineduntil the badge is shown; the slider'scomponentsobject is
internal.- The side sheet's and the dialog's close buttons reach a 48 x 48 target; an
<m-icon-button>'s icon keeps its size; a multiline text field uses the value it was
created with; a select's selected option has one look in both layers; the package no
longer carries a second README and licence underdist/.
Migrating from 0.10.x
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).- 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%; }).
Changed (breaking)
-
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.
Added
- 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
-
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. -
A list row takes the expressive shape. A row is 4 px at rest, 16 px on the list's outer corners, 12 px hovered, and 16 px focused, pressed or selected. With the default colours nothing changes at rest on screen: the row and the list are both the surface colour. Hover, focus and press look different, and rows given their own background show the corners. Set
--mtrl-list-item-shape,--mtrl-list-item-shape-outer,--mtrl-list-item-shape-hoverand--mtrl-list-item-shape-activeto0on the list, on any ancestor, or on the<m-list>element for square rows. M3 Lists specs: "Unselected corner radius: 4dp inner, 16dp outer" and "Selected corner radius: 16dp". -
The list's container is rounded, 16 px, by the same property as the rows' outer corners (
--mtrl-list-item-shape-outer). It is visible at rest on a list placed over a background that is not the surface colour. Set--mtrl-list-item-shape-outerto0on the list, on any ancestor, or on the<m-list>element to square the container and the rows' outer corners. M3 Lists specs, read 2026-10-03 (https://m3.material.io/components/lists/specs): "An expressive list has a segmented style and round corners". That page gives the item's corners and no value for the container. -
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.
Fixed
- A FAB in a corner keeps that corner in every state (#54). The state layer's mixin set
position: relativeon its host. Written beside the state selector, that is(0,2,0), so
a corner FAB's:hoveroutranked its corner class'sposition: fixed(0,1,0): computed
positionwasfixedat rest andrelativewhile hovered, focused or pressed, which took
the button out of its corner and into the page's flow. The mixin now writes the declaration
in:where(), which adds no specificity: any other position wins — the corner classes', or
the position a page gives its own fixed, absolute or sticky host — while a static host still
gets the position the layer's::beforeneeds. The side sheet's close button, which had no
position of its own, now carriesposition: relative: without it a page rule like
button { position: static }made it static and anchored its state layer and its 48px target
to the sheet's container. FAB and extended FAB, factory and element, four corners,
left-to-right and right-to-left, the same at rest, hovered, focused and pressed. - A text button with an icon no longer moves when it upgrades. The pre-upgrade
stylesheet now reserves the same inline padding as the upgraded button at the
default size and every explicit size, keeping the button and following content in
place. It had moved 4 px at the default,xsandssizes, and 24, 72 and 104 px
atm,landxl, where the layout-shift scores were 0.017, 0.091 and 0.180. - The text field's trailing icon button keeps its 48px target centred under
dir="rtl".
Thetouch-targetmixin anchored its::afterwithinset-inline-start: 50%and then moved it
with the physicaltranslate(-50%, -50%). Right-to-left, the logical inset pins the box's right
edge at the button's centre and the translate then pushes it another 24px left: the 48 x 48 box
sat with its centre 48px left of the button's centre and its right edge 4px left of the button's
left edge, so the button's hit area was its own 40px box (plus the engine's one pixel), measured
by walkingelementFromPointout from each edge in headless Chromium. The mixin now anchors with
physical offsets (left: 50%), as the icon button's own rule does, so the physical translate
centres it in both directions; left-to-right measures the same as before. Only the trailing-icon
target rule changes in the compiled sheet (2,744 rules before and after, one changed). The
mixin's only live user is the text field's trailing icon button, which only the factory renders
(the<m-text-field>element has no attribute for the label that makes the icon a button). - The side sheet's and the dialog's close buttons reach 48 x 48. Both were hand-built 40px
buttons with no expanded target, so a pointer 4px outside an edge — the outer band of the M3
target — hit nothing, left-to-right and right-to-left alike. Each now carries the icon button's
own mechanism: a::afterbox 48 x 48 centred on it, so the reachable target is 48 x 48 while
the button still paints 40 x 40, in place. M3 "Density": "The default target size should be at
least 48x48 CSS pixels." - A multiline text field uses the value it was created with.
createTextField({ type: 'multiline', value })wrote that string as avalueattribute. A textarea does not take its value from that attribute, sogetValue()was empty, the label stayed down, and a reset restored nothing. The value is now the textarea's default value, which is its text: the field shows it, the label floats, and a reset restores it. A single-line field still uses thevalueattribute.<m-text-field>no longer sets the default a second time. - An unlabelled switch is its 52 x 48 track box, not the label's row. With no label the root
kept the label's 12px gap, so it was 64px wide (12 + the 52px track) and 56px tall: in a 48px
slot the track ran 16px past the end and the checked 40px state layer 20px past it. The gap
belongs between a label and the track, and with no label the root is now the track's width and
48px tall with the 32px track centred. M3 "Switch" -> Specs -> Measurements gives the track
32x52dp and "Target: Size 48dp", and no height for a label row; its Accessibility section:
"Don't apply density to switches by default — this lowers their targets below our best
practice of 48x48 CSS pixels." So the unlabelled row is 48 tall, and a labelled switch keeps
its 56px row, unchanged. A switch with supporting text and no label is not unlabelled: its
helper stands where the label would, keeping the 12px gap and the 56px row. A label made only
of spaces is not empty, so the factory still treatslabel: " "as a labelled switch (the
element trims its text, so this is the factory only). Factory and element, left-to-right and
right-to-left. Before upgrade,
write the tag with nothing between its tags: a whitespace-only text node (a space, a line
break) is not:empty, so such a host keeps the labelled layout although the element then
builds no label. - A radio row grows with a wrapping label, and the circle stays centred on the label block. The row was a fixed 48px, so a three-line label painted 12px above and below it, and two adjacent wrapping labels overlapped. The row is now at least 48px (
min-height) with 4px of vertical padding, so it grows with the text and a one-line row stays 48px (circle 14px from the top and 10px from the inline start, text 12px from the top, 8px gap). The circle stays centred on the label block, as a labelled checkbox's box is. A horizontal group keepsalign-items: flex-startand sets noalign-self: options on one line share the start edge, and the next line starts after the tallest row. The pre-upgrade<m-radio>reserves the same minimum (min-height: 48px); a short label stays 48px. - An unlabelled radio is centred in its 48px target. The factory always appends
.mtrl-radios__text, and an empty label still took the 8px inline-start margin, so the 40px control sat flush at the start: the 20px circle's centre was at 20 rather than 24 (inset 10/18) and the state layer's inset was 0/8. An empty text span now takes no space. The row would then be 40px wide, so the unlabelled row setsmin-width: 48pxand centres the control. The circle's inset is 14/14 and the state layer's inset is 4/4, in both directions. A label made only of spaces is not empty, so the factory still treatslabel: " "as a labelled option (the element trims its text, so this is the factory only). Material 3 radio button, Specs, Measurements: icon size 20dp, state layer size 40dp, target size 48dp. - An unlabelled checkbox centres its box in its 48px target, state layer inside. The 18px
box sat flush at the inline-start (start inset 0, end inset 30), so the 40px state layer
(::before, centred on the box) spanned −11 to 29 — 11px outside the target — and the
focus ring reached 16px before the root's edge. Factory and element, left-to-right and
right-to-left. With no label the root is its own target, so the box now keeps 15px on both
sides (M3 "Icon alignment Center-aligned", "Target size 48dp") and the state layer
("State-layer size 40dp") lies 4px inside the root on all sides. A labelled checkbox, whose
root hugs box, gap and label, is unchanged. Before upgrade, write the tag with nothing between
its tags: a whitespace-only text node (a space, a line break) is not:empty, so such a host
keeps the labelled layout although the element then builds no label. - An
<m-icon-button>'s icon keeps its size token. The element's inner<button>kept Chrome's
default padding,1px 6px, because the page reset'sbutton { padding: 0 }is not in the shadow
root's adopted stylesheets, while the factory, which the page's global stylesheet does reach,
computes0px. Where the container left no room for icon and padding, the icon, a shrinkable flex
item, was drawn under its token: outlined xs narrow 14 against 20, outlined s narrow 18 against 24,
filled, tonal and standard xs narrow 16 against 20 and their s narrow 20 against 24, outlined xs
default width 18 against 20. The component stylesheet the element adopts now repeats the reset. - The package no longer contains a second copy of the README and licence under
dist/. - Select: the menu, measured against the field in every layer. Three defects from a
measurement of the select, in the factory's default layer (the menu inside the select's
element), withlayer: "top", and in<m-select>:- Right to left, the selected option's check mark was drawn over its text. It stayed at
the item's right, where right-to-left text begins: 18px of overlap. It is at the item's
end in both directions. The same holds for a selected item of any menu. - The menu is its field's width in both layers. In the default layer it kept a menu's
280px maximum, so under a 400px field it was 280px wide and stopped 120px short; in the
top layer it was 400px. The rule is one: the menu is as wide as its field (Compose's
exposed dropdown matches its anchor's width), and never under the 112px the M3 site gives
a menu as its minimum. - The selected option has one look, the M3 token's: secondary-container with
on-secondary-container text (md.comp.menu.list-item.selected.container.colorand
.label-text.color). In the top layer, and so in<m-select>, it was the primary colour
at 12% with primary text.
- Right to left, the selected option's check mark was drawn over its text. It stayed at
- A button's asymmetric icon padding mirrors in right-to-left. A size
sbutton with a leading icon, and a text button atxsorswith a leading icon, keep 12px before the icon and 16px after the label in both directions. Underdir="rtl"those insets had stayed physical, so the start side was 16px and the end side 12px. The insets are logical and follow the direction the icon already follows, including into a shadow root whosedirancestor is outside it. <m-text-field>in a right-to-left page is mirrored. Adir="rtl"on an
ancestor is outside the element's shadow root, where the stylesheet's[dir]selectors do
not reach, and the class that stands in for it was set for the outlined variant only: a
filled<m-text-field>kept its left-to-right layout (label, icons, prefix and suffix on
the wrong side, the text's padding unswapped), and an outlined one mirrored its label and
its outline's corners only. A field now takes themtrl-text-field--rtlclass from its
computed direction in both variants, and the stylesheet mirrors on that class or on an
ancestor'sdir. The same holds for a text field built by the factory inside a shadow
root of your own. In the light DOM a plain filled field is still mirrored by the
stylesheet alone, with no style read: it reads its direction only inside a shadow root,
once, in the task that created it. A plain filled field that is attached to a shadow
root in a later task than the one that created it, or whose host is not yet connected
when that task ends, is not mirrored until you callupdatePositions(). A direction
changed afterwards is likewise picked up only by the field's next placement
(updatePositions(), or a setter that places), as it was for the outlined variant; the
field does not watch for either. Also fixed, in the light DOM too: right to left, a
field with both a leading and a trailing icon padded its text 16px on the leading icon's
side, under the icon; it is 52px on both.preupgrade:checkfails a row when a sibling moves more than 0.5px on either axis,
regardless of its layout-shift score. Known movements name the sibling, axis and signed
measured value with a 0.5px tolerance; other movements in that row still fail. Filtered
runs enforce the same checks, and a disappeared defect requires removing its exception.
Consumer-typography switch cases also reject host movement or resizing over 0.5px.
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.
npm: material@3.0.0-next.1 · 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.