Repository navigation
Material Plus 1.2.0
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 — wearsFADEfrominternal/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 otherwiseMPTextField's shell wearing a chevron snapped its list onto the page while the field around it eased. Its positioner also takesPORTAL_LAYERnow, which is the same three utilities plus themp-portalhook every other portalled element carries for a host to hang its own reset off.- The keyboard cursor was invisible on the chosen row of
MPSelectandMPCombobox. Both wrote it asdata-highlighted:bg-mp-on-surface/8straight on the row, whereMPMenu— the same object with the same rows — puts anMPStateLayerunder it. A background can only be replaced, and a chosen row already has one, so the highlight anddata-selected:bg-mp-secondary-containerwere twobackground-colorutilities 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 andMPSegmentedButton's glyphs swapped withdisplay, 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 onopacitynow, 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:hiddenandMPIcon's owninline-flexare 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
MPDrawercomes in from the edge it is attached to, andMPDialoggrows as it fades. These are the two surfaces that take the whole page, and they now shareSHEET_MOTION:medium4onemphasized-deceleratearriving,short4onemphasized-accelerateleaving, 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. MPSnackbarcomes 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 istranslaterather thantransform, because Base UI writes a swiped plate's offset onto the inlinetransformand 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.widthrather thanscale-x, because acorner-fullcircle stretched horizontally is an ellipse where one that widens is a pill at every frame.- An arriving
MPTabPanelfades 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. MPFloatingActionButtontravels between its two widths.extendedis a prop callers change, most often on a scroll, and MD3 draws the disc becoming a stadium. There is no interpolating a definite width towardsauto, so the distance is split: the button raises its ownmin-width, padding and gap, and the label's track is a single grid column travelling between0frand1fr— which resolves to exactly the width the label needs at one end and to nothing at the other, rather than to amax-widthguess 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-labelnames 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 onshort2and both switch the transition off while the control is being dragged, or the handle would trail the pointer by its own duration.MPBadgegrows 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.visibilitystays as what hides it, joined by the scale rather than replaced by it: a badge parked atopacity: 0is still visible to find-on-page, andvisibilityhappens 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 aninvisiblebadge 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-namerather than by keying the rows, because akeywould 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 separaterowgroups 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 withmotion-safe:rather than negating it withmotion-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 anMPSliderhandle catching up with a value the keyboard moved: an arrow key held down repeats faster than 200ms, so a handle onshort4would 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.