Speed1×
Slow motion multiplier
Reduced motion
Show
Motion library compatibility
CSS-first1. Overlays cannot exit-animate
blockerCauseDialog is built on the native <dialog> element and every layer rides the Popover API. Both hard-remove the element from the top layer the moment it closes, so the exit animation runs against a box that is no longer painted. This is the same root cause as the CSS exit gap — one mechanism, failing two ways.
FixA keepMounted escape hatch: the consumer owns unmount, Astryx stops removing the element the frame the state flips. This is the pattern Motion documents for Base UI, so it is a shape consumers already recognise.
CostShares its fix with the presence work. Doing them separately means solving top-layer retention twice.
Same mechanism as the CSS exit gap: see it fail natively. One fix closes both, and doing them apart means solving top-layer retention twice.2. CSS transitions on transform
blockertransition: transform — today
Press it, then run the drag. The CSS transition re-eases every transform the library writes, so the drag lags the pointer and a spring never settles.
transition: scale — the middle path
Press feedback on the independent scale property, so it no longer collides with the library’s transform writes. Both work — this is the middle path worth prototyping.
no transition — what the brief proposes
No press feedback at all. The library is happy, and every consumer who never installs one loses the affordance.
CauseMotion performs all layout animation by writing transform on each frame. A CSS transition on transform turns every one of those writes into the start of a new 175ms ease, so the element chases the value it was already given.
FixDrop transform from the transition list where it only carries press or chevron feedback, or move that feedback onto the independent scale property, which no longer collides with a library writing transform.
| Component | Site | Declaration |
|---|---|---|
| Banner | core/Banner/Banner.tsx:340 | transitionProperty: 'transform' |
| BottomSheet | core/BottomSheet/BottomSheetPanel.tsx:144 | transitionProperty: 'transform, opacity' |
| Button | core/Button/Button.tsx:91 | transitionProperty: 'background-image, background-color, color, opacity, transform' |
| Chat | core/Chat/ChatDictationButton.tsx:70 | transitionProperty: 'transform, background-color' |
| Chat | core/Chat/ChatLayoutScrollButton.tsx:71 | transitionProperty: 'opacity, transform, max-width' |
| CodeBlock | core/CodeBlock/CodeBlock.tsx:207 | transitionProperty: 'transform' |
| Collapsible | core/Collapsible/Collapsible.tsx:107 | transitionProperty: 'transform' |
| ComplexSelector | core/ComplexSelector/ComplexSelector.tsx:128 | transitionProperty: 'background-image, background-color, color, opacity, transform' |
| ComplexSelector | core/ComplexSelector/ComplexSelector.tsx:154 | transitionProperty: 'transform' |
| DateInput | core/DateInput/TouchDateField.tsx:371 | transitionProperty: 'transform' |
| Lightbox | core/Lightbox/Lightbox.tsx:185 | transitionProperty: 'transform' |
| MobileNav | core/MobileNav/MobileNav.tsx:157 | transitionProperty: 'transform' |
| MultiSelector | core/MultiSelector/MultiSelector.tsx:187 | transitionProperty: 'transform' |
| MultiSelector | core/MultiSelector/MultiSelector.tsx:208 | transitionProperty: 'background-image, background-color, color, opacity, transform' |
| Overlay | core/Overlay/OverlayScrim.tsx:58 | transitionProperty: 'opacity, visibility, transform' |
| Resizable | core/Resizable/ResizeHandle.tsx:194 | transitionProperty: 'opacity, background-color, transform' |
| Selector | core/Selector/Selector.tsx:209 | transitionProperty: 'transform' |
| Selector | core/Selector/Selector.tsx:230 | transitionProperty: 'background-image, background-color, color, opacity, transform' |
| SideNav | core/SideNav/SideNavCollapseButton.tsx:53 | transitionProperty: 'transform' |
| SideNav | core/SideNav/SideNavItem.tsx:140 | transitionProperty: 'transform' |
| Stepper | core/Stepper/Step.tsx:367 | transitionProperty: 'transform' |
| Switch | core/Switch/Switch.tsx:288 | transitionProperty: 'transform, width, height' |
| TabList | core/TabList/TabMenu.tsx:165 | transitionProperty: 'transform' |
| Table | core/Table/plugins/groupedRows/useTableGroupedRows.tsx:178 | transitionProperty: 'transform' |
| Table | core/Table/plugins/rowExpansion/useTableRowExpansion.tsx:86 | transitionProperty: 'transform, color' |
| Table | core/Table/plugins/tree/useTableTreeData.tsx:266 | transitionProperty: 'transform' |
| Toast | core/Toast/Toast.tsx:83 | transitionProperty: 'opacity, transform' |
| TopNav | core/TopNav/TopNavMegaMenu.tsx:114 | transitionProperty: 'transform' |
| TopNav | core/TopNav/TopNavMegaMenu.tsx:131 | transitionProperty: 'opacity, transform, overlay, display' |
| TopNav | core/TopNav/TopNavMegaMenu.tsx:222 | transitionProperty: 'transform' |
| TopNav | core/TopNav/TopNavMenu.tsx:101 | transitionProperty: 'transform' |
| TopNav | core/TopNav/TopNavMenu.tsx:185 | transitionProperty: 'transform' |
| TreeList | core/TreeList/TreeListItem.tsx:219 | transitionProperty: 'transform' |
| Drawer | lab/Drawer/Drawer.tsx:157 | transitionProperty: 'transform' |
CostSmall and mechanical, and materially smaller than the brief budgets for.
3. No composition escape hatch
blockerCauseAstryx has deliberately refused polymorphism, so there is no asChild-style prop. Wrapping works for a root; it is unavailable for every compound part whose element Astryx owns internally — a DropdownMenu item, a Tab, a TableRow.
FixA composition prop, or a render-prop shape that fits the house conventions better. This is a design review rather than a fix, and it is the one blocker that cannot be closed inside the motion project.
CostUnsized. It unblocks router links and drag-and-drop as well, so it should not be argued on motion alone.
Cheap wins
What moves Astryx from “wrappable with caveats” to Motion-friendly for everything except overlay exits. None of these needs the composition decision.Add ref to the overlay components that lack itA component a library cannot get a DOM node out of cannot be animated by it at all. Everything else on this page is moot for those components.
Drop the transform transitionsCloses blocker 2 for every consumer, and removes a transition nobody outside a press interaction is asking for.34 sites across 23 components
Ship the JS token mirrorMotion takes seconds as a number and easing as a four-number array. It cannot resolve var(--duration-fast), so today a consumer either reads computed style or invents its own timing.Already a brief deliverable; the lab has it at /motion/js-mirror
Export a reduced-motion hookAn app with its own reduce-motion setting can flip Motion but cannot flip a CSS media query, so the two halves of one interface disagree. A shared hook is the only way the CSS half and the JS half can be told the same thing.Blocked on degrade-or-delete: the hook has to return a policy, not a boolean
What already works
Astryx is structurally better placed for this than most CSS-first systems, and the part that usually needs a rewrite is the part that is already right.okRefs are React-19 native
No forwardRef ceremony for the components that pass one through.okclassName and style pass through, consumer last
The consumer wins the merge, which is what a library needs to write to an element.okNo runtime CSS-in-JS
Nothing is recomputing styles underneath a library that is writing them.okStyleX loses the specificity contest by construction
Atomic classes versus inline styles: Motion wins every time, without a single !important. This is the compatibility that matters most and it already works.