Repository navigation
Material Plus 1.0.0
Fixed
-
MPTextField'sonFormResetwas skipped for the one change an IME makes. It is documented as running "before every change, ahead ofonChange", and it did — from theinputhandler. But a committed composition reachesonChangedown its own path, fromcompositionend, and browsers disagree on which of the two fires first: where the composition ends before theinputevent,compositionendwas the only announcement the parent ever got, and it carried no reset with it. A form-level error a further edit had made stale therefore cleared itself for somebody typingabcand stayed on screen for somebody typing 안녕 — the callback's whole purpose, withheld from exactly the readers this library keeps a composition path for.compositionendnow resets ahead of its change like every other route does. -
A focus ring that arrived a fifth of a second late. The text field's outline was transitioning its
border-widthfrom 1px to 2px, and an interpolated width is rounded to whole device pixels at paint — so the ring rendered as 1px for the whole animation and then jumped. Measured: the focus state itself landed in 4ms,data-focused, the label colour and the outline colour all changed at 4ms, and the width did not move until 197ms. The width now snaps and only the colour eases, which puts the ring on screen at 5ms. Anything sub-pixel in extent should not be transitioned; that is the general lesson and it is written next to the rule.
Changed
-
CONTAINER_SURFACEinsrc/internal/surface.tsis now the one container ladder.MPEmpty,MPListandMPTableeach carried their own copy of the five neutral surface roles a container paints itself at, and the copies had already drifted — two of them foldedtext-mp-on-surfaceinto every row and the third did not. Nine components read it now. The ink is deliberately left out of the shared table: most callers wanton-surfaceand say so once beside it, but a table sets its ink per cell, so a ladder that decided for them would be a ladder they had to undo. No class name, rendered attribute or prop changed. -
@keyframes mp-progress-segment-waveis nowmp-wave, and servesMPChatBubble's typing dots as well asMPProgressBox's segments. The two differ only in speed and stagger, which is what the two classes set; a second keyframe saying the same thing is a second chance for them to drift. No class name changed, so no component changed with it. -
The documentation's component groups are now
inputs,displayandfeedback, grouped by what a component does rather than by what it looks like.actions/is gone — a button collects an intent the same way a field collects a value, and splitting the two putMPButtonandMPSegmentedButtonin a different section from the controls they sit beside in a form.navigation/is gone for the same reason: a breadcrumb and a link show something and nothing more, which is whatdisplaymeans. Every page moved; no URL outside/components/changed, and the pages themselves are unchanged apart from their cross-links. -
The default source colour is a deep azure,
#00639b, rather than Material's baseline purple. A library defaulting to#6750a4makes a louder statement than it looks — purple is the one hue nobody arrives at by accident, so an application that has not chosen a colour ends up looking like it chose that one. The azure was picked for its chroma as much as its hue (0.118, against the baseline's 0.130), because every role takes its saturation from the seed and a more vivid blue pulls the whole scheme brighter than any reference Material palette. The tone table itself is unchanged and is still calibrated against the baseline palette. -
MPTextField's notched outline moved intosrc/internal/FieldOutline.tsxand is now shared withMPSelectandMPNumberField. No prop, no class name and no rendered attribute changed; the outline gainedgroup-focus-withinalongsidegroup-data-focused, so focus inside the shell — a number field's steppers, the password toggle — lights the ring too. -
--_mp-tone-primaryis now--_mp-tone-accent. It was never the primary family's alone: MD3 reads all three source-derived palettes at the same tones. Private, and not a name to set — the supported way to disagree with a colour is still to set the role. -
MPTextField'slargeboolean is nowsize, takingxs | sm | md | lg | xlat 32/40/56/64/72px.mdis Material's own 56px and the default, so the ladder is centred rather than extended upward — nobody has to know the scale exists to be handed the specification's size.
Added
-
The documentation site publishes an
llms.txt—/llms.txt, and/ko/llms.txtfor the Korean docs. One Markdown file naming every page, linking it, and carrying the same one-line summary the page's<meta description>does, so a language model asked about this library reads the map instead of guessing a prop name. It is generated at build time from the sidebar, next torobots.txtand for the same reason: the sidebar is the only structure that already knows the reading order, the titles and the links at once, and a hand-kept list of sixty pages goes wrong the first week nobody remembers to touch it — wrong in the way that is hardest to notice, a component whose page still loads under a name it no longer has. The changelog page gained adescriptionof its own with it, being the one page whose opening paragraph is not about the page but about whichever release is newest. -
Ten components:
MPBox,MPCard,MPCollapsible,MPAccordion,MPCarousel,MPDrawer,MPPopover,MPChatBubble,MPPillandMPSpoiler. Where MD3 defines the component the specification is followed to the number — a card iscorner-mediumwithfilled,elevatedandoutlinedas the specification's own three variants, a modal navigation drawer issurface-container-lowat 360dp withcorner-largeon its free edge, a popover is the menu'ssurface-containerat elevation 2 — and where it does not, the component is built out of the specification's own roles rather than out of sizes of the library's own. Four of them take a Base UI primitive underneath (Collapsible,Accordion,Dialog,Popover); the rest have no state to delegate. -
filledon a container is neutral, and every container refusescolor. This is the one place thevariantvocabulary means two things, and it is deliberate: on a component that is the thing being colouredfilledis the accent, and on a box holding somebody else's content it is MD3's own filled card —surface-container-highest. Dyeing a container dyes their content's background, so every link, field and button inside it would need an on-accent treatment of its own.MPBox,MPCard,MPCollapsible,MPAccordionandMPCarousel's frame therefore take nocolorat all: a prop that reaches nothing is a prop that has to be supported forever. -
corner-large, the sixth shape token. The sheet said there was nolarge"because nothing reads one"; a drawer's free edge now does, which is MD3's own corner for a navigation drawer — 12px is barely a corner on a 360dp panel and 28px is the bottom sheet's, which is a different object. Both shape presets gain the rung. -
Two more message namespaces,
chatandspoiler, in the same eighteen languages as the pickers. A chat bubble's five delivery marks are read out and never drawn, and a thread is a column of forty of them — a caller handing five words over per message would hand over the English ones. A spoiler's are the opposite and belong here for the opposite reason: they are the words on the cover, and a cover in a language the page is not in is a cover nobody reads. -
MPPillandMPChatBubbleare the library's own two shapes here, and neither is in the specification. A pill is the state a page has that is not about any one control — a call in progress, an upload still going — which is neither a snackbar (it happened and left), an alert (it is in the flow) nor a badge (it counts something on a control). Its stadium corner is the one place a sheet in this library is allowedcorner-full, because it is not a sheet lying on the page but an object hovering over it; opening its details moves the corner tocorner-extra-large, becausecorner-fullon a box six lines tall eats the first two words of every line. A chat bubble's tail is a cut corner rather than a drawn triangle, written as the logical corner so an Arabic thread squares the other one without being told. -
Eight components:
MPAlert,MPIconButton,MPDatePicker,MPDateRangePicker,MPDateTimePicker,MPTimePicker,MPAspectRatioandMPPanes. The pickers are drawn on MD3's own docked date-picker numbers — a 40dp cell, a filled circle for the chosen day, an outlined one for today — and their trigger isMPTextField's notched shell, so a date field in a form is the same object as the fields around it.MPIconButtonadds almost nothing toMPButtonon purpose: an icon-only button is already square and alreadycorner-full, so the disc falls out of the button's own tokens, and what the component actually contributes is a requiredlabel. -
A time picker made of columns rather than MD3's dial, which is the largest deliberate departure in the library and is documented as one. A dial is a pointer control with no keyboard or screen-reader path, which is exactly why the specification also ships a separate "time input" mode for those readers — leaving a component with two implementations of one question, of which the accessible one is the one nobody sees. Columns answer both readers with one control, in the library's own shapes. The bounds are checked at the granularity of the column being drawn, so a
minTimeof 09:30 leaves the hour9available and greys out the minutes before it; the naive whole-instant check hides the 9 and makes half past nine unreachable. -
MPLocaleProvider, and the localisation split behind it. Month names, weekday initials, AM/PM, date order and which day the week starts on come fromIntl, which speaks every language the platform does; the handful of words the platform has no opinion about — "Previous month", "Today", "Hour", "Dismiss" — come from a table insrc/internal/i18n.tstranslated into eighteen languages. A tag the table does not carry is therefore not a dead end: the calendar is still in that language and only those words fall back to English.navigator.languageis deliberately never consulted, because a value that differs between the server rendering the markup and the browser hydrating it is a hydration mismatch in the one part of the page a reader is looking at. -
No date library, and no typing into a picker's trigger. The whole of the arithmetic is
Dateand lives insrc/internal/date.ts; every name comes fromIntl. Quietly addingdate-fns— or worse, picking a side in the dayjs/luxon/Temporal argument on a consumer's behalf — would be a decision that was not this package's to make, and it still has one runtime dependency. Typing goes with it: parsing a date out of free text cannot be done honestly without such a library, and a field that understands27/7/26in one browser and not the next is worse than one that never claimed to. Every picker submits through a hidden input in the shape its native counterpart submits, in local time —toISOString()on aDatestanding for 15 July in Seoul reports the 14th, which is the single most expensive bug a date picker can ship. -
MPWeekday, the only new entry in the public type vocabulary. It isDate's numbering (0is Sunday) rather than CLDR's, because every calendar here is built out ofgetDay()and a second numbering would be a conversion at every comparison. -
Eleven components:
MPHighlight,MPDialog,MPOverlay,MPSnackbar,MPProgressLinear,MPProgressCircular,MPProgressBox,MPColorPicker,MPCombobox,MPMenuandMPOtpField. Where MD3 defines the component the specification is followed to the number — a dialog issurface-container-highatcorner-extra-largeand elevation 3, a menu issurface-containerat elevation 2 with 48dp rows, a snackbar isinverse-surfacewith its action ininverse-primary, a linear indicator is a 4dp groove and a circular one is a 48dp ring — and where it does not, the component is built out of the specification's own roles rather than out of sizes of the library's own. -
The component is called
MPSnackbar, notMPToast. Material has a name for it and that is the name. "Toast" is Android's older, non-interactive notification — no action, not dismissible, not part of Material Design 3 — so aToasthere would be a different component wearing the name of one the specification still ships. It is a provider plus auseMPSnackbar()hook rather than a component to keep mounted, because what a caller has at the moment a message is warranted is a click handler, not a place in the tree. -
MPProgressBoxis the library's own third shape, and is not in the specification. MD3 has a bar and a ring and stops there. A bar and a ring both say "this much is done" — a quantity a reader measures; a row of four segments says "this is step three", which is a quantity they count, and counting is faster than measuring for any number small enough to count. It is drawn out of the specification's own parts all the same, so a row of them sits in a Material page without announcing that it is extra. -
Four more colour roles, taking the sheet to twenty-nine:
surface(read by an overlay'ssolidtone),surface-container-high(a dialog's sheet),inverse-primary(a snackbar's action) andscrim.inverse-primaryis the accent palette read at the other scheme's tone — which is not an approximation of MD3's role but the definition of it, so the two numbers behind it are the accent stops above and below, swapped.scrimis the one colour in the sheet that is the same in both schemes, because a scrim is not a surface that has to be legible against the page: it is the absence of the page. -
corner-extra-largeand elevation level 3. Both are read by exactly one thing, the dialog, which is MD3's own answer for it: 28px is the loudest radius in the system, and a sheet that has taken the page is allowed to look like an object rather than like a panel. There is nolargebetweenmediumandextra-large, because nothing reads one. -
The first keyframes in the stylesheet, and they all belong to the progress indicators. An indeterminate indicator is a loop, which is the one thing a utility cannot be. All of them stop under
prefers-reduced-motionand rest on a partial frame rather than a full one — an indicator that has stopped must not read as an indicator that has finished. -
src/internal/color.ts, a hundred lines of colour arithmetic written out rather than installed.MPColorPickeris the only component that has to compute a colour rather than name one, and this is the whole of what it needs — three representations, the conversions between them, one parser and one formatter. HSV is the model the panel is drawn in and it never leaves: round-tripping through RGB would lose the hue of every greyscale colour, and the rail would snap to red the moment the pointer reached a corner. The package still has one runtime dependency. -
Fifteen components:
MPTypography,MPDivider,MPTextLink,MPBlockquote,MPAvatar,MPBadge,MPChip,MPSkeleton,MPEmpty,MPList,MPTable,MPTimeline,MPBreadcrumb,MPShortcutandMPTooltip. Where MD3 defines the component the specification is followed to the number — a chip is 32dp atcorner-small, a badge iserrorunderon-error, a list row is 56dp, a data-table row is 52dp, a plain tooltip isinverse-surfaceat elevation 0 — and where it does not, the component is built out of the specification's own roles rather than out of sizes of the library's own. Four of them take no primitive underneath on purpose: a divider, a badge, a quote and a timeline have no state and no keyboard contract, and reaching for a widget primitive to draw one hands a consumer's plain markup the semantics of a widget. -
MPTypographybrings the display and headline half of the type scale. Everylevelis one of MD3's own roles at the specification's size, leading, tracking and weight — which means the headings are weight 400, because Material's are. It is deliberately not on the size ladder:MPSizeis a control ladder, and a paragraph has no height to pick from a scale. -
inverse-surfaceandinverse-on-surface, taking the colour sheet to twenty-five roles. The neutral palette read at the other end of the scheme, and read by exactly one thing — a plain tooltip, whose whole job is to be legible over content it was never designed against. -
Eight more type roles —
display-small,headline-large/-medium/-small,title-large/-small,label-medium/-small— every one of them read by a component, none added in anticipation.label-smallat 11px is the smallest role in the specification and is why a badge's count is legible at all. -
MPAlign,MPSideandMPCornerinsrc/types.ts. The first and last are logical (start/end, notleft/right), because a divider's label and a badge's corner both move with the writing direction;MPSideis physical, because it is the axis a popup travels along and a tooltip above its trigger is above it in every language. -
A shared
VISUALLY_HIDDEN, andSHEET_*on the scale. Five components now put a sentence where a mark stands — the name behind an avatar's initials, "opens in a new tab",⌘announced as Command — and four share one sheet padding ladder, becausemdhas to mean 16px of padding on every sheet in the library. -
Ten components:
MPButton,MPButtonGroup,MPSegmentedButton,MPSelect,MPNumberField,MPCheckbox,MPRadioGroup,MPSwitch,MPSliderandMPFilePicker. Each is drawn from Material Design 3's own component tokens and delegates its behaviour to a Base UI primitive, so what is written here is the surface: the colours per variant and state, the shape, the state layer, and the parts of a control that are always assembled by hand. Three of them — the select, the number field and the text field — now share one notched-outline shell, because a form where the quantity box is a different height or radius from the boxes around it is a form that looks assembled rather than designed. -
Material's state layer, as a component. MD3 does not express hover, focus and press as more colours; it puts a translucent wash of the content colour over the container, 8% hovered and 10% focused or pressed. That is why it is an element rather than a
hover:bg-…: a background can only be replaced, and replacing it is what makes a text button's hover state opaque and an outlined button's cover its own border. A layer composites, so whatever was underneath is still there. -
Eighteen more colour roles, taking the sheet to twenty-three — the four accent families in full (
on-*,*-container,on-*-container), three surface containers, andoutline-variant. The three source-derived families are one palette generation read three times, so there is one accent tone stop rather than three and a chroma multiplier per family. The container andon-roles pass the palette's chroma through unmodified and let the browser's own gamut mapping do the clipping, at constant lightness and hue, exactly as HCT's does — which lands twelve of them within 0.01 of Material's baseline scheme from one number each. -
label-largeandtitle-mediumon the type scale,corner-small/medium/fullon the shape scale, elevation levels 1 and 2, and thestandardeasing curve. Every one of them is read by a component; none was added in anticipation. -
MPVariantandMPOrientationinsrc/types.ts, and avariantsection in Prop conventions.variantandcolorare shared vocabulary rather than members ofMPStyleProps: a component takes them when they mean something on it, and neither means anything on a text field. -
A size ladder, as the shared vocabulary.
MPSizeand a newMPStylePropsbundle insrc/types.ts. This is the one place the library knowingly goes beyond the specification: Material defines a single size per component because it describes a design system for whole products, and a component library gets used in places a design system does not plan for. A smaller control moves down the Material type scale rather than to an interpolated size of the library's own, which is whybody-mediumjoined the token sheet. -
A
design/section in the documentation, in both locales. Colour — the roles, the derivation, the ΔE against Material's reference palette, and the three levels of override — and Prop conventions, which defines the shared vocabulary and the rule each axis follows. -
The colour page's palette table reads its values back out of the browser. Both schemes at once, from two off-screen probes. Half of what it documents is an
oklch(from …)expression whose declared value tells a reader nothing, and a hardcoded table would go stale the first timesrc/styles.csswas touched. -
All components is a gallery of running previews rather than a table of links. Each card holds the real component, in whichever scheme the frame's switch is set to.
-
--mp-sys-typescale-body-medium-*, read by the field atxsandsm.
Material UI is gone. The library now implements Material Design 3 directly instead of building on @mui/material, which changes what it is: an implementation of the specification rather than an extension of somebody else's. Everything about how a component is used stays the same — no prop was renamed or removed — but the install, the theming and the styling model are all different.
Breaking
- Peer dependencies are one instead of three.
@mui/material,@emotion/reactand@emotion/styledare no longer peers;@base-ui/react1 is. It is a peer rather than a dependency because it carries React context — a consumer'sFormhas to be able to see a field from here, and that only works with one copy in the tree. - There is no provider, and no MUI theme is read. A
ThemeProvideraround these components no longer does anything to them. Colours, type, shape and motion come from CSS custom properties instead, so theming moved from JavaScript to CSS. CssBaselineis no longer assumed. The library adds no page-level styling of any kind and no longer expects the page to have a reset: each component resets what it owns on the element it owns — a<button>'s browser-default background, a control's font, which a native form control does not inherit.MPColoris MD3's four accent roles (primary,secondary,tertiary,error) rather than Material UI's six. The specification's colour system has noinfo,successorwarning, and offering them would promise roles the token sheet cannot derive.--mp-durationbecame--mp-sys-motion-duration-short4, and is 200ms rather than 250ms — MD3'sshort4, which is what a text field's label and outline transition on.
Added
- Theming from a single source colour.
--mp-source-colorgenerates every colour role the way Material generates a scheme from a source colour. The tone stops are read off MD3's own baseline ref palette rather than chosen by eye: fed the baseline source, the derivation lands on the baseline scheme —primaryanderroron its exact hex in both schemes, the near-grey roles within a ΔE of 0.008, below anything visible. What the default source colour changes is the hue and chroma the ramp is applied to, never the ramp. - Coexistence with an existing Material setup. Each role reads
--md-sys-color-*before falling back to its own derivation, and the library never writes that namespace. A project running Material Web is picked up with no configuration. The full order is--mp-sys-color-*(an explicit override) →--md-sys-color-*(the page's own) → derived. - Dark mode with nothing to configure.
prefers-color-schemeis followed by default, anddata-mp-scheme="dark"or.darkon any element drives it explicitly. Both schemes are the same tonal palettes read at different tones, which is how MD3 defines them — so a source colour moves light and dark together and there is no second set of values to keep in sync. - Scoped and runtime theming. Every token is an ordinary inherited custom property, and the derived roles are declared on
*rather than on:rootso each element re-resolves them against whatever is in scope where it sits. A section with its own source colour works, a.darkon<body>works, and a colour a reader picks at runtime is an inline style with no re-render.
Changed
MPTextFieldis Base UI'sFieldplus the notch, the adornments and the password toggle. The notched outline is a<legend>interrupting a<fieldset>'s top border, which is a native behaviour, so the gap is sized to the label with no measuring in JavaScript. State styling reads thedata-focused,data-invalidanddata-disabledattributes Base UI emits, which keeps it in CSS.- The IME composition handling — the reason the component exists — is unchanged.
- Token names follow MD3 (
primary,on-surface-variant,corner-extra-small,body-large) rather than Material UI's palette model (main/light/dark/contrastText), which is a different and earlier colour system. - Only the tokens a component actually reads exist. MD3 defines around fifty colour roles; an outlined text field reads five, and the rest are absent until something needs them.
Removed
- No CSS-in-JS runtime. Styling is a stylesheet, so there is nothing to inject on render and no hydration pass for theme styles.
Every release of this package is in the changelog on the docs site and in CHANGELOG.md at this tag.