Skip to content

material 3.0.0-next.1

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 03 Oct 09:55
· 449 commits to main since this release
0482363

The 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/core and its seven areas (canvas,
    compose, dom, shapes, state, theme, utils). material/core/compose/features no
    longer resolves: import the same names from material/core/compose.
  • ProgressComponent no longer has canvas, resize, track, indicator and buffer
    (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.badge may be undefined until the badge is shown; the slider's components object 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 under dist/.

Migrating from 0.10.x

  • material/core/<area> only. A path under an area no longer resolves:
    mtrl/core/compose/features becomes material/core/compose, which exports the same names
    (withLifecycle among 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 for createSelect(
    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" imported withLifecycle() from (in 0.10.x too).
    The map now names material/core and its seven areas: material/core/canvas, /compose,
    /dom, /shapes, /state, /theme and /utils. Any other path under material/core
    throws ERR_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 function

    Every name material/core/compose/features exported is an export of
    material/core/compose, except withBadge and the types BadgeComponent and
    BadgeConfig of that feature, which no document named: they are internal; for a badge,
    use createBadge from material. LabelManager, the type of LabelComponent's label,
    was only in the nested path and is now exported from material/core/compose.

  • Progress: canvas, resize, track, indicator and buffer are no longer on
    ProgressComponent.
    They named how the indicator is drawn (one canvas; track,
    indicator and buffer were 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. Reading progress.canvas in TypeScript is now an error (TS2339). Migration:
    for the canvas, progress.element.querySelector('canvas'); nothing replaces resize(),
    as the component observes its own size; setBuffer() and getBuffer(), 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; chips and
    divider had 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 its display: none input inside;
    it is now not rendered at all, as an element with the hidden attribute is not. Its value still
    submits with its form and still reads from value. 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's font-size: 24px; line-height: 2 the 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-hover and --mtrl-list-item-shape-active to 0 on 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-outer to 0 on 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.md is 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, hasStyles and SHADOW_BASE_STYLES (from material/elements), the API
    for writing custom elements of your own on material's machinery, are tagged
    @experimental in 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 the define…() 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 components bag is internal. The slider's controller reads an older
    components object as a fallback for its elements; nothing in the library fills it, the
    public SliderComponent never 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 of SliderComponent) and slider.element.

  • Tabs: tab.badge may be undefined until the badge is shown. The type always allowed
    it (badge?: BadgeComponent); it is now the documented contract, on tab.badge and 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 first setBadge(), or from creation with the badge option. Use
    setBadge(), getBadge(), showBadge() and hideBadge(), which work whether the badge
    exists yet or not, and check tab.badge for undefined before 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-space is 4, and Compose's ButtonDefaults.ExtraSmallIconSpacing is 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: relative on its host. Written beside the state selector, that is (0,2,0), so
    a corner FAB's :hover outranked its corner class's position: fixed (0,1,0): computed
    position was fixed at rest and relative while 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 ::before needs. The side sheet's close button, which had no
    position of its own, now carries position: 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, xs and s sizes, and 24, 72 and 104 px
    at m, l and xl, 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".
    The touch-target mixin anchored its ::after with inset-inline-start: 50% and then moved it
    with the physical translate(-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 walking elementFromPoint out 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 ::after box 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 a value attribute. A textarea does not take its value from that attribute, so getValue() 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 the value attribute. <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 treats label: " " 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 keeps align-items: flex-start and sets no align-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 sets min-width: 48px and 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 treats label: " " 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's button { padding: 0 } is not in the shadow
    root's adopted stylesheets, while the factory, which the page's global stylesheet does reach,
    computes 0px. 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), with layer: "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.color and
      .label-text.color). In the top layer, and so in <m-select>, it was the primary colour
      at 12% with primary text.
  • A button's asymmetric icon padding mirrors in right-to-left. A size s button with a leading icon, and a text button at xs or s with a leading icon, keep 12px before the icon and 16px after the label in both directions. Under dir="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 whose dir ancestor is outside it.
  • <m-text-field> in a right-to-left page is mirrored. A dir="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 the mtrl-text-field--rtl class from its
    computed direction in both variants, and the stylesheet mirrors on that class or on an
    ancestor's dir. 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 call updatePositions().
    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:check fails 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).
    material 3.0.0-next.0 and mtrl 0.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.