Skip to content

Material Plus 1.0.0

Choose a tag to compare

@jooy2 jooy2 released this 04 Oct 08:03
· 485 commits to main since this release

Fixed

  • MPTextField's onFormReset was skipped for the one change an IME makes. It is documented as running "before every change, ahead of onChange", and it did — from the input handler. But a committed composition reaches onChange down its own path, from compositionend, and browsers disagree on which of the two fires first: where the composition ends before the input event, compositionend was 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 typing abc and stayed on screen for somebody typing 안녕 — the callback's whole purpose, withheld from exactly the readers this library keeps a composition path for. compositionend now 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-width from 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_SURFACE in src/internal/surface.ts is now the one container ladder. MPEmpty, MPList and MPTable each carried their own copy of the five neutral surface roles a container paints itself at, and the copies had already drifted — two of them folded text-mp-on-surface into every row and the third did not. Nine components read it now. The ink is deliberately left out of the shared table: most callers want on-surface and 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-wave is now mp-wave, and serves MPChatBubble's typing dots as well as MPProgressBox'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, display and feedback, 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 put MPButton and MPSegmentedButton in 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 what display means. 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 #6750a4 makes 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 into src/internal/FieldOutline.tsx and is now shared with MPSelect and MPNumberField. No prop, no class name and no rendered attribute changed; the outline gained group-focus-within alongside group-data-focused, so focus inside the shell — a number field's steppers, the password toggle — lights the ring too.

  • --_mp-tone-primary is 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's large boolean is now size, taking xs | sm | md | lg | xl at 32/40/56/64/72px. md is 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.txt for 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 to robots.txt and 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 a description of 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, MPPill and MPSpoiler. Where MD3 defines the component the specification is followed to the number — a card is corner-medium with filled, elevated and outlined as the specification's own three variants, a modal navigation drawer is surface-container-low at 360dp with corner-large on its free edge, a popover is the menu's surface-container at 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.

  • filled on a container is neutral, and every container refuses color. This is the one place the variant vocabulary means two things, and it is deliberate: on a component that is the thing being coloured filled is 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, MPAccordion and MPCarousel's frame therefore take no color at 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 no large "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, chat and spoiler, 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.

  • MPPill and MPChatBubble are 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 allowed corner-full, because it is not a sheet lying on the page but an object hovering over it; opening its details moves the corner to corner-extra-large, because corner-full on 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, MPAspectRatio and MPPanes. 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 is MPTextField's notched shell, so a date field in a form is the same object as the fields around it. MPIconButton adds almost nothing to MPButton on purpose: an icon-only button is already square and already corner-full, so the disc falls out of the button's own tokens, and what the component actually contributes is a required label.

  • 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 minTime of 09:30 leaves the hour 9 available 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 from Intl, 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 in src/internal/i18n.ts translated 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.language is 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 Date and lives in src/internal/date.ts; every name comes from Intl. Quietly adding date-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 understands 27/7/26 in 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 a Date standing 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 is Date's numbering (0 is Sunday) rather than CLDR's, because every calendar here is built out of getDay() and a second numbering would be a conversion at every comparison.

  • Eleven components: MPHighlight, MPDialog, MPOverlay, MPSnackbar, MPProgressLinear, MPProgressCircular, MPProgressBox, MPColorPicker, MPCombobox, MPMenu and MPOtpField. Where MD3 defines the component the specification is followed to the number — a dialog is surface-container-high at corner-extra-large and elevation 3, a menu is surface-container at elevation 2 with 48dp rows, a snackbar is inverse-surface with its action in inverse-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, not MPToast. 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 a Toast here would be a different component wearing the name of one the specification still ships. It is a provider plus a useMPSnackbar() 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.

  • MPProgressBox is 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's solid tone), surface-container-high (a dialog's sheet), inverse-primary (a snackbar's action) and scrim. inverse-primary is 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. scrim is 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-large and 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 no large between medium and extra-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-motion and 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. MPColorPicker is 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, MPShortcut and MPTooltip. Where MD3 defines the component the specification is followed to the number — a chip is 32dp at corner-small, a badge is error under on-error, a list row is 56dp, a data-table row is 52dp, a plain tooltip is inverse-surface at 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.

  • MPTypography brings the display and headline half of the type scale. Every level is 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: MPSize is a control ladder, and a paragraph has no height to pick from a scale.

  • inverse-surface and inverse-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-small at 11px is the smallest role in the specification and is why a badge's count is legible at all.

  • MPAlign, MPSide and MPCorner in src/types.ts. The first and last are logical (start/end, not left/right), because a divider's label and a badge's corner both move with the writing direction; MPSide is 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, and SHEET_* 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, because md has to mean 16px of padding on every sheet in the library.

  • Ten components: MPButton, MPButtonGroup, MPSegmentedButton, MPSelect, MPNumberField, MPCheckbox, MPRadioGroup, MPSwitch, MPSlider and MPFilePicker. 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, and outline-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 and on- 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-large and title-medium on the type scale, corner-small/medium/full on the shape scale, elevation levels 1 and 2, and the standard easing curve. Every one of them is read by a component; none was added in anticipation.

  • MPVariant and MPOrientation in src/types.ts, and a variant section in Prop conventions. variant and color are shared vocabulary rather than members of MPStyleProps: 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. MPSize and a new MPStyleProps bundle in src/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 why body-medium joined 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 time src/styles.css was 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 at xs and sm.


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/react and @emotion/styled are no longer peers; @base-ui/react 1 is. It is a peer rather than a dependency because it carries React context — a consumer's Form has 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 ThemeProvider around 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.
  • CssBaseline is 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.
  • MPColor is MD3's four accent roles (primary, secondary, tertiary, error) rather than Material UI's six. The specification's colour system has no info, success or warning, and offering them would promise roles the token sheet cannot derive.
  • --mp-duration became --mp-sys-motion-duration-short4, and is 200ms rather than 250ms — MD3's short4, which is what a text field's label and outline transition on.

Added

  • Theming from a single source colour. --mp-source-color generates 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 — primary and error on 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-scheme is followed by default, and data-mp-scheme="dark" or .dark on 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 :root so each element re-resolves them against whatever is in scope where it sits. A section with its own source colour works, a .dark on <body> works, and a colour a reader picks at runtime is an inline style with no re-render.

Changed

  • MPTextField is Base UI's Field plus 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 the data-focused, data-invalid and data-disabled attributes 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.