Skip to content

Material Plus 1.2.0

Choose a tag to compare

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

A pass over every component asking one question: when this thing changes, does it change, or does it cut? Fourteen places cut. The list below is what each of them does now, and the reasoning where the answer was not simply "add a transition".

Fixed

  • MPSelect's popup appeared with no animation at all. Every other portalled surface in the library — the menu, the popover, the combobox's list, the dialog, the drawer, the overlay, the snackbar, the colour picker's panel — wears FADE from internal/surface.ts. The select's list was written with a positioner class of its own and never picked it up, so a control that is otherwise MPTextField's shell wearing a chevron snapped its list onto the page while the field around it eased. Its positioner also takes PORTAL_LAYER now, which is the same three utilities plus the mp-portal hook every other portalled element carries for a host to hang its own reset off.
  • The keyboard cursor was invisible on the chosen row of MPSelect and MPCombobox. Both wrote it as data-highlighted:bg-mp-on-surface/8 straight on the row, where MPMenu — the same object with the same rows — puts an MPStateLayer under it. A background can only be replaced, and a chosen row already has one, so the highlight and data-selected:bg-mp-secondary-container were two background-color utilities of equal specificity with the winner decided by the order Tailwind sorted them in. The selected fill won: the list gave the arrow keys nothing to follow on the row it opens on. A layer composites instead — and, unlike a background, fades.
  • MPSwitch's and MPSegmentedButton's glyphs swapped with display, which cannot be animated at all. The switch thumb travels its groove and changes size over 200ms and the glyph riding in it cut from a cross to a tick partway through; a segment's container and ink cross to the selected treatment over the same 200ms and the tick was stamped on top of that. Both pairs cross-fade on opacity now, stacked rather than laid out side by side, because for the length of a fade both are drawn. It also retires the hazard the old wrappers existed for: hidden and MPIcon's own inline-flex are both display utilities, so which won came down to stylesheet order.
  • MPColorPicker's tick jumped between swatches and its hover elevation had nothing to arrive on. Choosing a swatch does not change the swatch, so the mark moving is the feedback, and a mark that arrived in one frame read as a flicker. It is drawn on every swatch now and transparent on all but one — the only arrangement it can fade from, since a mark that is not in the DOM has nothing to travel out of.

Changed

  • The checkbox's mark and the radio's dot grow in. Both containers already eased and the half of the control that says what the answer is was the half that snapped. They start from different places, and the difference is what each of them is: a tick is a stroke, and from zero it spends its first frames as a smudge too small to read as a tick, so it starts at 60%; a disc at any size is still a disc, so the dot grows out of the middle of the ring the way MD3 draws it. Base UI holds each indicator mounted until its transition finishes, which is what gives them an exit as well as an entrance.
  • A modal MPDrawer comes in from the edge it is attached to, and MPDialog grows as it fades. These are the two surfaces that take the whole page, and they now share SHEET_MOTION: medium4 on emphasized-decelerate arriving, short4 on emphasized-accelerate leaving, which is MD3's own pair. The rule the other floating surfaces follow — a box full of text should not travel — is about opening at something the reader is aiming for, and neither of these does. A drawer arrives at an edge, which is the one part of the window nobody is reading; a dialog arrives at nothing. What a bare fade cost them was direction: a left drawer and a right one were the same event, and a dialog that faded was indistinguishable from a dialog that had always been there. The dialog grows from 95% rather than the specification's 80%, because growing is the one transition here that resamples text and 80% of a 560dp sheet starts it 56px narrower than it ends. A full-screen dialog has no middle to grow out of and only fades.
  • MPSnackbar comes in from the edge its stack is pinned to. The same argument, and the position is a fact the component already knew and was not saying while it mattered. One duration in both directions rather than the sheets' asymmetric pair: a snackbar often arrives behind two others, and an entrance long enough to read as an arrival makes a queue of three feel like it is buffering. The travel is translate rather than transform, because Base UI writes a swiped plate's offset onto the inline transform and a travel spelled the same way would be wiped out by the first flick.
  • MPBottomNavigation's active indicator widens out of a circle instead of one fill switching off as another switches on. The slot the glyph sits in cannot be the thing that grows — it is what holds the destination in place, and a row of five would shuffle sideways every time the reader moved between them — so the fill is a layer inside it, travelling between the slot's full width and a circle exactly as wide as the slot is tall. width rather than scale-x, because a corner-full circle stretched horizontally is an ellipse where one that widens is a pill at every frame.
  • An arriving MPTabPanel fades in. The indicator slid under the labels and the content it points at cut, so the bar animated its own decoration and snapped the thing the decoration is for. There is no matching fade out, and that is structural: Base UI keeps a leaving panel in the layout until its transition finishes, so an exit would put both panels in the flow and grow the page to hold the pair before collapsing onto the new one.
  • MPFloatingActionButton travels between its two widths. extended is a prop callers change, most often on a scroll, and MD3 draws the disc becoming a stadium. There is no interpolating a definite width towards auto, so the distance is split: the button raises its own min-width, padding and gap, and the label's track is a single grid column travelling between 0fr and 1fr — which resolves to exactly the width the label needs at one end and to nothing at the other, rather than to a max-width guess large enough for the longest label anyone might pass. The label therefore stays in the DOM while the button is a disc, clipped to nothing; aria-label names the button either way, so nothing is announced twice.
  • MPSlider's handle travels to a value it was not dragged to. A slider moves in two ways and only one of them wants a transition: an arrow key or a click on the track is a jump, and a drag is the handle being held. Both parts travel on short2 and both switch the transition off while the control is being dragged, or the handle would trail the pointer by its own duration.
  • MPBadge grows in and shrinks back out. A count that simply appeared is a count nobody saw change, which is the whole event a badge exists to report. visibility stays as what hides it, joined by the scale rather than replaced by it: a badge parked at opacity: 0 is still visible to find-on-page, and visibility happens to interpolate in exactly the shape this needs — visible from the first frame on the way in, visible until the last on the way out. That is what lets an invisible badge keep its count in the DOM, and it has to, because an element with nothing inside it has no size to shrink from.
  • Stepping a month turns the calendar's page. A grid that swapped in place said which month it was in the header and nothing about which way the reader had gone, on a control whose whole job is moving between them. A keyframe rather than a transition, because what changed is the content of six rows and there are no two states to interpolate; restarted by clearing animation-name rather than by keying the rows, because a key would unmount the cell a reader's focus is standing on in the middle of an arrow-key walk across a month boundary. Only the weeks turn — the weekday names above them are the same seven whatever month it is — so the header row and the weeks are separate rowgroups now, which is the arrangement a table has anyway.
  • The reduced-motion answer is a decision per component, as it is for the MPAnimate* effects. Where a travel would be the only thing lost, it is simply not declared — the drawer, the dialog and the snackbar ask for it with motion-safe: rather than negating it with motion-reduce:, which also avoids four pairs of classes of equal specificity settling their argument by stylesheet order. What is left in each case is exactly the fade those surfaces had before. The calendar's page turn is switched off in the same unlayered block the loading indicators use, leaving the swap. Controls that resize in place — a tick growing inside an 18dp box, a navigation pill widening, a FAB extending — are left alone, which is the line the switch thumb and the tab indicator already drew.

Added

  • --mp-sys-motion-duration-short2, at MD3's own 100ms. The first duration token here shorter than 200ms, added under the rule the token block states — the ones something in the library actually reads. Its one reader is an MPSlider handle catching up with a value the keyboard moved: an arrow key held down repeats faster than 200ms, so a handle on short4 would never finish one step before the next began.

Every release of this package is in the changelog on the docs site and in CHANGELOG.md at this tag.