Repository navigation
Releases: floor/material
Release list
material 3.2.4
Keyboard focus stays inside the surface you open and returns to the field you
chose from.
Fixes
-
An externally mounted
<m-dialog>restores its surface before opening
(#116).
Following thecreateDialogexample — appendingdialog.elementto
document.body, then callingdialog.open()— moved the surface out of
its overlay; opening then made every overlay sibling inert, including the
dialog itself. It was visible, but initial focus stayed onBODYand Tab
reached no action, while the unchanged default mount worked in the same
browser run. Before an overlay dialog opens, its surface now returns to its
own overlay when the caller mounted or detached it elsewhere; the move
happens afterbeforeopenhas accepted the request and leaves the native
top-layer path unchanged. Measured in Chromium across six mount paths —
unchanged, body-appended, nested, configured container, detached and native
top layer — each opens with initial focus on the Cancel button, Tab and
Shift+Tab wrap inside the dialog, Escape closes it with focus returned, the
background stops scrolling and restores it, an existinginertattribute
is preserved, and a remount reopens cleanly. -
Enter on a focused day in
<m-datepicker>saves the date and closes the
calendar (#117).
With a day focused, Enter fell through the key handler and activated the
day button as a native click. Default docked pickers committed the date but
stayed open; modal variants kept it as a draft and stayed open, and focus
stayed on the day instead of returning to the field. Enter is now handled
explicitly for single-date selection, through the existing commit and close
path, with the native click suppressed so one key press emits one change
and one close. It accepts independently ofcloseOnSelect, while pointer
and Space selection keep that option; range endpoints still select without
prematurely accepting, and modal ranges keep their confirmation action.
Measured in Chromium across 16 single-date cases — docked, modal,
modal-input calendar mode and full-screen, in document and shadow-root
placement, under bothcloseOnSelectsettings — each saves once, closes
the API and native dialog, clearsaria-expanded, and returns real focus
to the field; four range controls preserve endpoint selection and
confirmation.
Internal
- The two fixes cost 25 bytes gzipped in the all-js row (127,427 to
127,452) and nothing in the dialog's own: 13,496 bytes gzipped against its
unchanged 13,619-byte ceiling. No ceiling moved.
npm: material@3.2.4 · 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.
material 3.2.3
Elements hold their baseline at upgrade.
Fixes
-
<m-tabs>keeps its reserved baseline at upgrade (#108).
In a baseline-aligned row beside a switch and a text field, upgrading
<m-tabs>moved it down 12.90625px and moved its neighbors up 6.5px: the
empty pre-upgrade block synthesized a baseline at its bottom edge while the
upgraded block exported a different baseline from its first rendered tab,
though both boxes were already 48px tall. The tabs host now carries
contain: layout, so it keeps synthesizing its baseline from its own box;
paint overflow remains visible. Measured over 40 strict browser cases
(LTR/RTL, the default page typography and four overrides, text and icon
tabs, tabs-only and shared upgrades): the host shifts 0.00px and every
neighbor moves 0.00px, with the 48px and 64px heights held. -
<m-radios>reserves the upgraded baseline before upgrade (#109).
Beside a switch and a text field in a baseline-aligned row,<m-radios>
moved up 5px when the elements were defined: pre-upgrade the host exposed
the first row's label-text baseline, 29px down its box, and after upgrade
the first row's control baseline, 34px down. The undefined host is now the
column it becomes after upgrade, and its first item is a zero-width strut
ending on the circle's bottom edge (height: 34px; margin-block-end: -34px),
so the row holds the host in place with no horizontal change. The strut
follows an actual option —:has(> m-radio)— so an empty group, compact or
indented, reserves nothing and its neighbors do not move. Measured at the
default page typography and 12px/1, 12px/2, 24px/1 and 24px/2: the host
moves 0.00px and the row keeps its 144px height.
Internal
- The upgrade holds cost a few bytes and no ceiling moved. Layout
containment on the tabs host leaves the tab row at 13,642 bytes gzipped
against its unchanged ceiling; the radios guard adds 42 bytes across the
three pre-upgrade assets and 520 bytes unpacked with its packed source.
npm: material@3.2.3 · 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.
material 3.2.2
Components follow the page's direction, inside shadow roots and in bundled
output.
Fixes
-
Direction rules match inside a shadow root, at the nearest
[dir], and
follow live changes (#113).
Thertlmixin compiled to[dir='rtl'] &, and two sheets wrote that
selector out literally; a descendant selector cannot cross a shadow
boundary, so the page's<html dir="rtl">outside the element's shadow
tree never reached its rules, though every host inheriteddirection: rtl
and matched:dir(rtl). A shared direction-resolution module now sets
mtrl-rtl/mtrl-ltron the roots it watches, from the element's computed
style and, for a factory root, the nearest[dir]; the direction mixin
emits the class arms, a:host(...)arm and an attribute fallback, and a
document-levelMutationObserverfollowsdirchanges. Measured at 600px:
the drawer's start sheet moves 0→240, its item padding 16/24→24/16, the
dialog's close button x 528→32, and a subtree's nearer[dir]wins over the
page both ways. -
<m-slider>mirrors its value, segments and stops on a right-to-left
page (#114).
The axis was resolved withelement.closest("[dir]"), and a selector
cannot cross a shadow boundary: the host inheritsdirection: rtl, but the
page'sdirlies outside the shadow root, so the lookup returnednull
and every left-to-right position stayed. The axis now reads the element's
computed direction, which inherits into the shadow tree, and keeps the
nearest[dir]as the fallback wheredirectiondoes not resolve (JSDOM).
Measured on a 600px page,<m-slider value="40">: the handle moves
x 233→353, the segments swap (0/248→368/0), and the stop moves 590→6 — each
the mirror of its LTR box. -
A multi-browse carousel lays out and scrolls on a right-to-left page
(#115).
The carousel read and wrotescrollLeftin left-to-right arithmetic and
placed every item withtranslate3d(startPx, 0, 0), while Chromium reports
an RTL scroller'sscrollLeftfrom 0 at the inline start down to negative
values toward the left: the scroller settled at the wrong end and the items
landed mid-container. At 600px the main build settled atscrollLeft -232
with items at 8.91, 187.47 and 237.59. EveryscrollLeftread and write is
now normalised (rtl ? -x : x), items anchor to the track's right edge in
RTL, the translate is negated so the clip insets mirror with it, and the
arrow keys walk the way the page reads: the scroller starts at the
inline-start edge and the items sit at 314.67, 40 and -118.67, each the
mirror of its LTR box with the 2.66667px clip. -
The toolbar and date picker mirror in a consumer's bundled output
(#121).
_toolbar.scssand_datepicker.scsswrote their direction rules with
:dir(rtl), which a bundler lowers into:lang()lists: the lowered arms
select languages, not direction, so they matched no element on a
dir="rtl"page — the bundled sheet carried 12,768:lang()entries, 342
of them from these six rules. Both sheets now route the rules through the
direction mixin, so the toolbar keeps its hidden offset off the edge it
leaves, the date picker's range band starts at the selection's centre and
its month chevrons point the way the months go, in unbundled and bundled
output alike. -
A closed side sheet leaves the viewport on the page's logical edge
(#112).
Placement uses logicaljustify-content, which reverses in right-to-left,
but the closed sheet moved with physicaltranslateX(-100%/100%)and its
solid overshoot cover never reversed: the closed sheet stood inside the
viewport at x=256 and painted a 32px stripe back into the page. Both now
follow the logical end, so the closed sheet sits at x=-256 with its cover
at -32.
Internal
- Direction resolution in the shared kernel costs ~220 bytes per entry
(175-290 measured). - The carousel's direction fix costs ~+44 B gzip on the carousel row: the
at-rest direction flip now rebuilds through a direction observer. The two
ceilings that crossing required are raised rule-exact in the same change:
carousel 11,468 to 11,600, all-js 125,849 to 127,200. - The packed package's unpacked ceiling is raised to 6,524,000 (#121).
The toolbar and date picker declare their left-to-right direction values on
each component root, so a nested LTR instance keeps them across an RTL
parent's inherited custom properties; packed with CI's Node 22 / npm 10, the
package measures 6,459,061 unpacked. The raise follows the rule — measured
plus 1%, rounded up to the next 1,000.
npm: material@3.2.2 · 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.
material 3.2.1
No layout jump at upgrade.
Fixes
-
<m-button>keeps its box and baseline at upgrade (#42).
The upgraded host is now the control's own flex box —display:inline-flex; vertical-align:middle— the box the pre-upgrade rule already reserves. An
inline-blockhost builds its line box from the page's typography: at
font-size:24px; line-height:2the 48px strut stood 8px over the 40px control
and moved the whole line below it, and its last-line-box baseline sat
differently from the inline-flex box's first-item baseline (−7px at 24/2,
±1.3–1.8px at the other measured typographies, with or without a leading
icon). Height and baseline now hold on both sides of the upgrade at every
measured typography, and the button keeps the spec's height per size. -
Six more upgraded hosts stop growing with the page's line height (#51).
m-icon-button,m-button-group,m-split-button,m-datepicker(docked),
m-checkboxandm-selecthosts are their control's own flex box instead of
an inline-block line box, so the page's line strut no longer reaches the
control: at 24px/2 the icon button and the split button stood 48px tall over
40px controls, the group's pre-upgrade text item laid out a 48px line, the
docked datepicker carried a 99px line box over its 96px control, and the
checkbox and the select grew a pixel. The icon-button host keeps its
bottom-edge baseline through a zero-width stretch item, matching its
pre-upgrade box; every host holds its height and baseline across the upgrade. -
<m-button-group>reserves its sizes, density, spacing and icons before
upgrade (#49).
The pre-upgrade sheet now reserves every size, density, the standard and the
connected between-space, and text and icon buttons, in LTR and RTL, at the
five typographies the issue measures — of the 600 measured combinations, 590
moved on main (worst 401.66px); none moves now. The packed tarball outgrew
its ceiling: measured 1,078,362 bytes under Node 22.23.3 / npm 10.9.9, the
ceiling moves 1,078,000 → 1,090,000 bytes by the size script's own rule
(measured plus 1%, to the next thousand), the one budget this change
outgrew.
npm: material@3.2.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.
material 3.2.0
Components that tell you what's wrong.
Features
-
A text field with a
trailinglistener but an unlabeled trailing icon now warns in
development (#100).
The trailing icon's click only reaches an application when the icon is a button, and a button
needs a name: registering atrailinglistener — a factory or config callback, or a listener
added straight on<m-text-field>— without settingtrailingIconLabel/
trailing-icon-labelprints one warning that says to set it. The warning is shared across
subscriptions, rebuilds and reconnects, and the element's own bridge does not count as an
application listener. Production builds carry none of it: the diagnostic and its state are
stripped, so it costs no bytes and no runtime in what you ship. -
<m-split-button>dispatchesopenandclosewhen its menu opens and closes (#99).
The same names<m-menu>and<m-fab-menu>already use, with an empty detail, fired from the
host as the trailing button's menu expands and collapses: once per transition, whether the
close comes from choosing an item, Escape or a click outside.selectwith its{ value }
payload, the leading button's nativeclickand the factory surface are unchanged, and no
attribute or method is added.
Fixes
-
A badge given in a tab's config is attached, and it mirrors in RTL (#92).
A badge configured on a tab was dropped before; it now wraps the tab's icon, or its label when
there is no icon, inside the button, so fixed tab rows keep their widths. The badge mirrors to
the correct side in right-to-left, including in<m-tabs>under an inherited direction —
<html dir="rtl">or adir="rtl"container — through native:host(:dir(rtl))rules, with
no observer or listener, following livedirchanges. -
Baseline's
:rootdefaults no longer override a selected theme, in any import order (#101).
Baseline emitted its light and dark defaults on:rootand.dark-themeunconditionally, and
:rootand[data-theme="name"]share specificity, so an app compiling the theme partials
with baseline after its own themes got baseline's colours on themed pages — including
baseline's dark palette under a dark OS preference. The defaults are now emitted at zero
specificity (:where(:root),:where(.dark-theme)): a selected theme always wins, while the
roles a theme does not declare still inherit from baseline, and an unthemed page is unchanged.
Internal
- CI fails loudly when npm's
nextdist-tag falls behindlatest(#94).
Adist-tagsjob runs after every publish and re-reads the registry, failing the run — never
the publish, and never skipping the GitHub Release — whenmaterial@nextwould install an
older build thanmaterial@latest. The same check runs locally with
bun run check:dist-tags(or against a saved state with--tags <file>). CI only; nothing in
the package changed.
npm: material@3.2.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.
material 3.1.0
Added
-
Sass consumers can choose which contrast rules to compile with
$contrast-emit(#95).
Configurematerial/src/styles/themesormaterial/src/styles/mainwith@use … with
before loading other material theme modules.all(the default) emits explicit medium/high
contrast attributes and the system contrast preference;explicitemits only the attribute
rules;preferenceemits only the preference rules;noneemits neither. Every mode keeps
standard light and dark roles. In the twelve-theme bundle,nonesaves 117,184 raw bytes and
8,177 gzip bytes (level 9) compared withall, by dropping both contrast activation families.
Default output and prebuilt CSS are unchanged; other Sass configuration remains unsupported. -
<m-text-field>acceptstrailing-icon-labelto name its trailing icon button (#37).
With a non-empty label, the icon is a button that dispatchestrailingfrom the host on click,
Enter or Space. The event bubbles and is composed; its detail is{ value, event }, carrying
the field's current value and the original activation event. Changing the label updates the
existing button in place and keeps the input and focus. A missing or empty label leaves the
icon decorative: anaria-hiddenspan, outside the accessibility tree.
Documentation
- Added
MIGRATING.md, verified against published 3.0.5, and linked it from both READMEs (#97).
npm: material@3.1.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.
material 3.0.6
Fixed
- An extended FAB in a collapsed navigation rail's header shrinks to its icon button instead of
being clipped (#93).
A collapsed rail is 96 px wide, but an extended FAB placed in its header kept its expanded
width — padding, gap and a label up to 280 px — so the rail's edge cut it off and the label
read "Ad…". This held for a FAB built by the factory as well as for an<m-extended-fab>
element slotted into the header, because the element draws its button inside its own shadow
root. The rail's header now hands the collapsed state down as an inherited CSS custom property
that the extended FAB reads, so the button drops its padding, gap and label, stays at its size's
height, and fits inside the rail in both text directions. Expanding the rail restores the full
FAB. The FAB keeps compacting when its element is upgraded, replaced or rebuilt, and it fires
no new events; a FAB method you call yourself keeps the events it had. A standalone extended FAB
with the--collapsedmodifier draws the same compact button.
npm: material@3.0.6 · 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.
material 3.0.5
Changed
package.jsonadds five keywords:material 3 expressive,m3 expressive,expressive motion,spring motionandmotion. They are what npm's search reads. The library follows
the Material 3 Expressive tokens for sizes, shapes and colours, and takes the spring motion
tokens where a component's design uses them. The description, the README and the code are
unchanged.
Fixed
-
A discrete slider's handle rests on the end stop at either end of the track, not past it
(#79).
A discrete slider sets its stops in from the ends of the track by the end's own corner radius —
8, 12, 16 or 28 px at XS to XL — so its first and last stops sit inside the pill. The handle's
position dropped that inset at the first and last values, so at the minimum and the maximum its
centre sat on the track's very edge, a corner radius past the end stop, with the handle
straddling the end of the pill. Every value position now takes the inset, the ends included: the
handle's centre lands on the end stop, and the filled track stops with it, keeping the handle gap
it keeps everywhere else. A continuous slider spans the whole track as before; the interior
stops, every size and right-to-left are unchanged. -
A toolbar's overflow button draws its icon in the toolbar's own colour, not black (#81).
The overflow icon's SVG carried nofill, so it painted the SVG initial black in every variant
and both themes, while the toolbar's other icons follow the button through
fill="currentColor". On a dark or coloured toolbar the overflow button showed black dots. The
icon now carriescurrentColor, so it takes the colour its siblings take, in every theme and
under a customcolor. A customoverflowIconis drawn as you give it. -
A card whose header is its last child keeps the bottom padding of its other three sides
(#81).
A card header takes the card's 16 px padding on the top, left and right and no bottom padding, so
content or actions can follow it flush. A card that was only a header — no content, no actions —
kept that zero, and its title and supporting text sat on the card's lower edge. A header that is
the card's last child now keeps the same 16 px at the bottom as on the other sides; a header
followed by content or actions stays flush. -
A tooltip outside the top layer is placed correctly on a scrolled page (#84).
The surface is fixed to the viewport in every path and takes the target's viewport rectangle,
but the placement added the document's scroll offset to it: on a page scrolled 300 px, the
tooltip sat one scroll distance away from its target, and a page scrolled sideways pushed it
aside the same way. The placement no longer adds the scroll, so the surface lands on its
target at any scroll position, in every placement. The top-layer path, the viewport clamps and
the transitions are unchanged.
npm: material@3.0.5 · 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.
material 3.0.4
Fixed
-
A tooltip's arrow is the colour of the tooltip's own surface, in every variant and both
themes (#77).
The arrow grows out of the container, so it has to read as that surface. A rich tooltip paints
surface-container, but its arrow kept the baseinverse-surface; a plain tooltip paints
inverse-surface, but its arrow still painted thesurface-container-highof a surface the
variant had dropped. On each of the twelve placements that draw an arrow, the arrow showed as a
triangle in a colour that was not the tooltip's own. Every direction now takes its colour from
the variant's surface —inverse-surfaceby default and for plain,surface-containerfor rich.
Geometry, transitions and the surfaces themselves are unchanged. -
An uncontained carousel lays every item out at the configured item size, stepping by
itemWidth + gapand flowing past the container's edge (#78).
The uncontained layout was placed through the multi-browse keyline model: after the first one or
two items, slides were scaled down toward a 10 px anchor, so each showed as a sliver a few pixels
wide and consecutive items stepped 28.8 px apart instead of 288. That bunched the tail of the row
into a fraction of its width and left the row ending well before the container's right edge.
Items are now laid out at the configured width, atitemWidth + gapsteps, and cut only by the
container edge — never shrunk into a sliver. How much of an item the edge leaves showing depends
on the container width and the scroll position. An item that sits fully past the edge stays laid
out and is not hidden. Snap positions are those steps, so a slide's scroll offset is its index
timesitemWidth + gap; the scrollable extent is wider, because the row is now as long as its
slides. The contained variants — multi-browse, hero, center-aligned hero and fullscreen — keep
their keyline layout, and under reduced motion they behave as before.
npm: material@3.0.4 · 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.
material 3.0.3
Fixed
-
A selected toggle icon button inside a toolbar keeps its round shape.
A standalone toggle morphs round to square when selected (icon buttons specs),
and toolbar items inherited that morph, so the selected Bold of a formatting
toolbar turned a rounded square — a shape the toolbars specs warn against in
the floating container. Selection inside a toolbar is shown by colour roles
alone (the toolbar token set has no item shape tokens), so the round radius
now stays in every state. Extending that to the pressed state — and to docked
as well as floating toolbars — is this library's choice (round stays round),
not something the specs ask for. The pin is each item's own resting radius,
on the bar's direct items only, whatever the tag prefix the elements were
registered under — the toolbar's sheet matches the marker attribute every
icon-button host of this library carries. The radius is right at first
paint on the paths the checks cover: elements created in the browser,
renderElement()output, and the server render of the React, Vue, Svelte
and Solid adapters, which all write the marker into their markup. Marker-less
markup still pins under the defaultm-prefix, where the sheet can spell
the tag. Standalone toggles, square buttons, the overflow menu's content and
explicit--mtrl-button-shape-*overrides are unchanged. -
An internal marker attribute,
data-mtrl-icon-button, now appears on icon buttons; it carries no public API.
npm: material@3.0.3 · 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.