diff --git a/packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss b/packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss index 9638037113..915665b938 100644 --- a/packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss +++ b/packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss @@ -1,4 +1,5 @@ @import "~@vibe/style/dist/mixins"; +@import "~@vibe/style/dist/mixins/motion"; .contentWrapper { outline: 0; @@ -53,121 +54,147 @@ } // Animations +// +// Two flavors driven by Dialog's `animationType` prop: +// - opacity-and-slide: fade + 16px translate from origin (default for popovers) +// - expand: scale-from-origin + fade (used by Tooltip, Menu submenus) +// +// Both are gated on `prefers-reduced-motion: no-preference`. Reduced-motion +// users get the popover at rest immediately with no transition. $translate-minus-px: calc(var(--space-16) * -1); .opacitySlideAppear { - opacity: 0; + @include motion-safe { + opacity: 0; - &.top { - transform: translateY(var(--space-16)); - } + &.top { + transform: translateY(var(--space-16)); + } - &.right { - transform: translateX($translate-minus-px); - } + &.right { + transform: translateX($translate-minus-px); + } - &.bottom { - transform: translateY($translate-minus-px); - } + &.bottom { + transform: translateY($translate-minus-px); + } - &.left { - transform: translateX(var(--space-16)); + &.left { + transform: translateX(var(--space-16)); + } } } .opacitySlideAppearActive { - transition: opacity 0.2s ease, transform 0.2s ease-out; - opacity: 1; pointer-events: none; - &.top, - &.bottom { - transform: translateY(0); - } + @include motion-safe { + transition: opacity var(--motion-productive-long) var(--motion-timing-enter), + transform var(--motion-productive-long) var(--motion-timing-enter); + opacity: 1; - &.right, - &.left { - transform: translateX(0); + &.top, + &.bottom { + transform: translateY(0); + } + + &.right, + &.left { + transform: translateX(0); + } } } .expandAppear, .expandExit { - transition: transform 0.1s $expand-animation-timing; - &.top, - &.topStart, - &.topEnd { - transform-origin: bottom center; - transform: scale(0.8); - &.edgeBottom { - transform-origin: bottom left; - } - &.edgeTop { - transform-origin: bottom right; + @include motion-safe { + transition: transform var(--motion-productive-long) var(--motion-timing-enter), + opacity var(--motion-productive-long) var(--motion-timing-enter); + opacity: 0; + + &.top, + &.topStart, + &.topEnd { + transform-origin: bottom center; + transform: scale(0.92); + &.edgeBottom { + transform-origin: bottom left; + } + &.edgeTop { + transform-origin: bottom right; + } } - } - &.right, - &.rightStart, - &.rightEnd { - transform-origin: left; - transform: scale(0.8); - &.edgeBottom { - transform-origin: top left; - } - &.edgeTop { - transform-origin: bottom left; + &.right, + &.rightStart, + &.rightEnd { + transform-origin: left; + transform: scale(0.92); + &.edgeBottom { + transform-origin: top left; + } + &.edgeTop { + transform-origin: bottom left; + } } - } - &.bottom, - &.bottomStart, - &.bottomEnd { - transform-origin: top; - transform: scale(0.8); - &.edgeBottom { - transform-origin: top left; + &.bottom, + &.bottomStart, + &.bottomEnd { + transform-origin: top; + transform: scale(0.92); + &.edgeBottom { + transform-origin: top left; + } + &.edgeTop { + transform-origin: top right; + } } - &.edgeTop { - transform-origin: top right; - } - } - &.left, - &.leftStart, - &.leftEnd { - transform-origin: right; - transform: scale(0.8); - &.edgeBottom { - transform-origin: top right; - } - &.edgeTop { - transform-origin: bottom right; + &.left, + &.leftStart, + &.leftEnd { + transform-origin: right; + transform: scale(0.92); + &.edgeBottom { + transform-origin: top right; + } + &.edgeTop { + transform-origin: bottom right; + } } } } .expandExit { - transition: transform 0.1s $expand-animation-timing; + @include motion-safe { + transition: transform var(--motion-productive-medium) var(--motion-timing-exit), + opacity var(--motion-productive-medium) var(--motion-timing-exit); + } } .expandAppearActive { - transition: transform 0.1s $expand-animation-timing; pointer-events: none; - &.top, - &.topStart, - &.topEnd, - &.bottom, - &.bottomStart, - &.bottomEnd, - &.right, - &.rightStart, - &.rightEnd, - &.left, - &.leftStart, - &.leftEnd { - transform: scale(1); + @include motion-safe { + transition: transform var(--motion-productive-long) var(--motion-timing-enter), + opacity var(--motion-productive-long) var(--motion-timing-enter); + opacity: 1; + + &.top, + &.topStart, + &.topEnd, + &.bottom, + &.bottomStart, + &.bottomEnd, + &.right, + &.rightStart, + &.rightEnd, + &.left, + &.leftStart, + &.leftEnd { + transform: scale(1); + } } } diff --git a/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss b/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss new file mode 100644 index 0000000000..42e4f624af --- /dev/null +++ b/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss @@ -0,0 +1,61 @@ +@import "~@vibe/style/dist/mixins"; +@import "~@vibe/style/dist/mixins/motion"; + +// Submenu open/close animation. Mirrors the `expand` variant in DialogContent — +// fade + scale-from-origin keyed off the floating-ui placement so the submenu +// grows out of the parent menu item's edge. +// +// Uses CSS animations instead of transitions because CSSTransition+mountOnEnter +// can skip the "from" frame when the class swap lands in a single paint — +// keyframes fire reliably on a freshly mounted element. + +@keyframes submenuExpandIn { + from { + opacity: 0; + transform: scale(0.92); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes submenuExpandOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.92); + } +} + +.rightStart { + transform-origin: top left; +} + +.rightEnd { + transform-origin: bottom left; +} + +.leftStart { + transform-origin: top right; +} + +.leftEnd { + transform-origin: bottom right; +} + +.appearActive, +.enterActive { + @include motion-safe { + animation: submenuExpandIn var(--motion-productive-long) var(--motion-timing-enter) forwards; + } +} + +.exitActive { + @include motion-safe { + animation: submenuExpandOut var(--motion-productive-medium) var(--motion-timing-exit) forwards; + } +} diff --git a/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx b/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx index ef5e0a4b5a..1ee7c50966 100644 --- a/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx +++ b/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx @@ -1,9 +1,12 @@ import React, { useMemo, useRef } from "react"; +import { CSSTransition } from "react-transition-group"; +import { camelCase } from "es-toolkit"; import { DialogContentContainer } from "@vibe/dialog"; import { useFloating, flip, type Placement } from "@floating-ui/react-dom"; import { type MenuChild } from "../../../Menu/MenuConstants"; import { type MenuItemSubMenuProps } from "./MenuItemSubMenu.types"; -import { useIsomorphicLayoutEffect } from "@vibe/shared"; +import { useIsomorphicLayoutEffect, getStyle } from "@vibe/shared"; +import styles from "./MenuItemSubMenu.module.scss"; const DEFAULT_FALLBACK_PLACEMENTS: Placement[] = ["right-end", "left-start", "left-end"]; @@ -16,6 +19,7 @@ const MenuItemSubMenu = ({ submenuPosition }: MenuItemSubMenuProps) => { const childRef = useRef(null); + const transitionRef = useRef(null); useIsomorphicLayoutEffect(() => { if (!autoFocusOnMount || !open || !childRef?.current) { @@ -49,24 +53,36 @@ const MenuItemSubMenu = ({ return null; } + const placementClassName = getStyle(styles, camelCase(actualPlacement)); + return ( -
- {subMenu && open && ( - - {React.cloneElement(subMenu, { - ...subMenu?.props, - isVisible: open, - isSubMenu: true, - onClose, - ref: childRef, - useDocumentEventListeners: !autoFocusOnMount - })} - - )} +
+ +
+ + {React.cloneElement(subMenu, { + ...subMenu?.props, + isVisible: open, + isSubMenu: true, + onClose, + ref: childRef, + useDocumentEventListeners: !autoFocusOnMount + })} + +
+
); }; diff --git a/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/__tests__/MenuItemSubMenu.test.tsx b/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/__tests__/MenuItemSubMenu.test.tsx index 923d5224d3..2a2e8947f3 100644 --- a/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/__tests__/MenuItemSubMenu.test.tsx +++ b/packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/__tests__/MenuItemSubMenu.test.tsx @@ -24,24 +24,24 @@ const MockMenuChild = React.forwardRef(({ onClose }: MenuProps, ref: React.Forwa Object.assign(MockMenuChild, { isMenu: true }); describe("MenuItemSubMenu", () => { - it("should render correctly with visibility hidden when not open", () => { + it("should not render submenu content when not open", () => { const mockAnchor = document.createElement("div"); - const { container } = render( + const { queryByText } = render( ); - expect(container.firstChild).toHaveStyle("visibility: hidden"); + expect(queryByText("Items")).not.toBeInTheDocument(); }); - it("should render correctly and become visible when open is true", () => { + it("should render submenu content when open is true", () => { const mockAnchor = document.createElement("div"); - const { container } = render( + const { getByText } = render( ); - expect(container.firstChild).toHaveStyle("visibility: visible"); + expect(getByText("Items")).toBeInTheDocument(); }); it("should call onClose when requested to close", () => { diff --git a/packages/core/src/components/Modal/Modal/Modal.module.scss b/packages/core/src/components/Modal/Modal/Modal.module.scss index 6483378b2f..29f16568de 100644 --- a/packages/core/src/components/Modal/Modal/Modal.module.scss +++ b/packages/core/src/components/Modal/Modal/Modal.module.scss @@ -1,3 +1,6 @@ +@import "~@vibe/style/dist/mixins"; +@import "~@vibe/style/dist/mixins/motion"; + $full-view-margin: 40px; .container { @@ -113,129 +116,131 @@ $full-view-margin: 40px; } } -// FROM state — must match 0% keyframes exactly to prevent flash on mount. +// FROM state — must match keyframes exactly to prevent flash on mount. +// Only applied under motion-safe, otherwise the modal mounts directly at rest. .containerEnter { - .overlay { - opacity: 0; - } + @include motion-safe { + .overlay { + opacity: 0; + } - .centerPop { - opacity: 0; - transform: translate(-50%, -50%) scale(0.8); - } + .centerPop { + opacity: 0; + transform: translate(-50%, -50%) scale(0.94); + } - .anchorPop { - opacity: 0; - transform: translate(calc(-50% + var(--modal-start-x, 0px)), calc(-50% + var(--modal-start-y, 0px))) scale(0.8); - } + .anchorPop { + opacity: 0; + transform: translate(calc(-50% + var(--modal-start-x, 0px)), calc(-50% + var(--modal-start-y, 0px))) + scale(0.94); + } - .fullView { - opacity: 0.3; - transform: translateY(30px); + .fullView { + opacity: 0.3; + transform: translateY(var(--space-24)); + } } } .containerEnterActive { - .overlay { - opacity: 1; - transition: opacity 100ms cubic-bezier(0, 0, 0.4, 1); - } + @include motion-safe { + .overlay { + opacity: 1; + transition: opacity var(--motion-productive-medium) var(--motion-timing-enter); + } - .centerPop { - animation: centerPopIn 150ms cubic-bezier(0, 0, 0.4, 1) forwards; - } + .centerPop { + animation: centerPopIn var(--motion-expressive-short) var(--motion-timing-emphasize) forwards; + } - .anchorPop { - animation: anchorPopIn 200ms cubic-bezier(0, 0, 0.4, 1) forwards; - } + .anchorPop { + animation: anchorPopIn var(--motion-expressive-short) var(--motion-timing-emphasize) forwards; + } - .fullView { - animation: fullViewIn 250ms cubic-bezier(0, 0, 0.4, 1) forwards; + .fullView { + animation: fullViewIn var(--motion-expressive-short) var(--motion-timing-enter) forwards; + } } } .containerExitActive { - .overlay { - opacity: 0; - transition: opacity 100ms cubic-bezier(0.6, 0, 1, 1); - } + @include motion-safe { + .overlay { + opacity: 0; + transition: opacity var(--motion-productive-medium) var(--motion-timing-exit); + } - .centerPop { - animation: centerPopOut 100ms cubic-bezier(0.6, 0, 1, 1) forwards; - } + .centerPop { + animation: centerPopOut var(--motion-productive-long) var(--motion-timing-exit) forwards; + } - .anchorPop { - animation: anchorPopOut 150ms cubic-bezier(0.6, 0, 1, 1) forwards; - } + .anchorPop { + animation: anchorPopOut var(--motion-productive-long) var(--motion-timing-exit) forwards; + } - .fullView { - animation: fullViewOut 100ms cubic-bezier(0.6, 0, 1, 1) forwards; + .fullView { + animation: fullViewOut var(--motion-productive-medium) var(--motion-timing-exit) forwards; + } } } @keyframes centerPopIn { - 0% { + from { opacity: 0; - transform: translate(-50%, -50%) scale(0.8); + transform: translate(-50%, -50%) scale(0.94); } - 50%, - 100% { + to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes centerPopOut { - 0%, - 50% { + from { opacity: 1; transform: translate(-50%, -50%) scale(1); } - 100% { + to { opacity: 0; - transform: translate(-50%, -50%) scale(0.8); + transform: translate(-50%, -50%) scale(0.94); } } @keyframes anchorPopIn { - 0%, - 40% { + from { opacity: 0; - transform: translate(calc(-50% + var(--modal-start-x, 0px)), calc(-50% + var(--modal-start-y, 0px))) scale(0.8); + transform: translate(calc(-50% + var(--modal-start-x, 0px)), calc(-50% + var(--modal-start-y, 0px))) + scale(0.94); } - 100% { + to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes anchorPopOut { - 0% { + from { opacity: 1; transform: translate(-50%, -50%) scale(1); } - 60%, - 100% { + to { opacity: 0; - transform: translate(calc(-50% + var(--modal-start-x, 0px)), calc(-50% + var(--modal-start-y, 0px))) scale(0.8); + transform: translate(calc(-50% + var(--modal-start-x, 0px)), calc(-50% + var(--modal-start-y, 0px))) + scale(0.94); } } @keyframes fullViewIn { - 0% { + from { opacity: 0.3; - transform: translateY(30px); + transform: translateY(var(--space-24)); } - 33% { - opacity: 1; - } - - 100% { + to { opacity: 1; transform: translateY(0); } @@ -249,6 +254,6 @@ $full-view-margin: 40px; to { opacity: 0; - transform: translateY(30px); + transform: translateY(var(--space-24)); } } diff --git a/packages/core/src/components/Skeleton/Skeleton.module.scss b/packages/core/src/components/Skeleton/Skeleton.module.scss index 2d2f09b818..3f31c2ac59 100644 --- a/packages/core/src/components/Skeleton/Skeleton.module.scss +++ b/packages/core/src/components/Skeleton/Skeleton.module.scss @@ -1,20 +1,25 @@ +@import "~@vibe/style/dist/mixins"; +@import "~@vibe/style/dist/mixins/motion"; @import "SkeletonVariables"; @mixin shine-animation() { overflow: hidden; - animation-duration: 0.8s; - animation-direction: alternate; - animation-iteration-count: infinite; - animation-name: shine; - animation-timing-function: steps(10, end); - - @keyframes shine { - 0% { - opacity: 0.4; - } - 100% { - opacity: 1; - } + + @include motion-safe { + animation: shine 0.8s steps(10, end) infinite alternate; + } + + @include motion-reduce { + opacity: 0.7; + } +} + +@keyframes shine { + 0% { + opacity: 0.4; + } + 100% { + opacity: 1; } } diff --git a/packages/core/src/components/Tabs/Tab/Tab.module.scss b/packages/core/src/components/Tabs/Tab/Tab.module.scss index 250aeee5e2..92a3801731 100644 --- a/packages/core/src/components/Tabs/Tab/Tab.module.scss +++ b/packages/core/src/components/Tabs/Tab/Tab.module.scss @@ -1,4 +1,5 @@ @import "~@vibe/style/dist/mixins"; +@import "~@vibe/style/dist/mixins/motion"; @import "../../../styles/states"; @import "../../../styles/typography"; @@ -66,7 +67,10 @@ .tabWrapper.active:after { transform: scaleX(1); - transition: transform var(--motion-productive-medium) var(--motion-timing-enter); + + @include motion-safe { + transition: transform var(--motion-productive-medium) var(--motion-timing-enter); + } } .tabWrapper.disabled .tabInner { diff --git a/packages/core/src/components/Toast/Toast.module.scss b/packages/core/src/components/Toast/Toast.module.scss index b2f051443b..b57d5a2221 100644 --- a/packages/core/src/components/Toast/Toast.module.scss +++ b/packages/core/src/components/Toast/Toast.module.scss @@ -1,3 +1,5 @@ +@import "~@vibe/style/dist/mixins"; +@import "~@vibe/style/dist/mixins/motion"; @import "../../styles/keyframes"; .toast { @@ -17,7 +19,11 @@ border-radius: var(--border-radius-small); color: var(--fixed-light-color); transform: translateX(-50%); - transition: background-color 80ms cubic-bezier(0.6, 0, 0.4, 1), width 200ms cubic-bezier(0, 0, 0.4, 1); + + @include motion-safe { + transition: background-color var(--motion-productive-short) var(--motion-timing-transition), + width var(--motion-expressive-short) var(--motion-timing-enter); + } &.typeNormal { background-color: var(--primary-color); @@ -69,9 +75,14 @@ .actionButton.withTransition { opacity: 0; - animation: bounceIn 150ms cubic-bezier(0, 0, 0.4, 1); - animation-delay: 300ms; - animation-fill-mode: forwards; + + @include motion-safe { + animation: bounceIn var(--motion-productive-long) var(--motion-timing-emphasize) 300ms forwards; + } + + @include motion-reduce { + opacity: 1; + } } .content { @@ -85,19 +96,19 @@ } .enterActive { - animation-iteration-count: 1; - animation-fill-mode: forwards; - animation-name: slideIn; - animation-duration: 350ms; - animation-timing-function: cubic-bezier(0.6, 0, 0.4, 1); + @include motion-safe { + animation: slideIn var(--motion-expressive-short) var(--motion-timing-emphasize) forwards; + } } .exitActive { - animation-iteration-count: 1; - animation-fill-mode: forwards; - animation-name: slideOut; - animation-duration: 350ms; - animation-timing-function: cubic-bezier(0.6, 0, 0.4, 1); + @include motion-safe { + animation: slideOut var(--motion-productive-long) var(--motion-timing-exit) forwards; + } + + @include motion-reduce { + opacity: 0; + } } .closeButton { @@ -105,42 +116,37 @@ } @keyframes slideIn { - 0% { - transform: translate(-50%, -100px); - } - - 40% { - transform: translate(-50%, 16px); + from { + transform: translate(-50%, -100%); + opacity: 0; } - 100% { - transform: translate(-50%, 0px); + to { + transform: translate(-50%, 0); + opacity: 1; } } @keyframes slideOut { - 0% { + from { transform: translate(-50%, 0); + opacity: 1; } - 100% { - transform: translate(-50%, -100px); + to { + transform: translate(-50%, -100%); opacity: 0; } } @keyframes bounceIn { - 0% { - transform: scale(0.8); + from { + transform: scale(0.85); opacity: 0; } - 50% { - opacity: 1; - } - - 100% { - opacity: 1; + to { transform: scale(1); + opacity: 1; } } diff --git a/packages/docs/src/pages/components/Menu/Menu.interactions.ts b/packages/docs/src/pages/components/Menu/Menu.interactions.ts index 5e031c5f0f..8a06e56d2c 100644 --- a/packages/docs/src/pages/components/Menu/Menu.interactions.ts +++ b/packages/docs/src/pages/components/Menu/Menu.interactions.ts @@ -1,4 +1,4 @@ -import { userEvent, within } from "@storybook/test"; +import { userEvent, waitFor, within } from "@storybook/test"; import { getByRole, getByText, @@ -45,12 +45,14 @@ const showSubSubMenusOnHover = async canvas => { await userEvent.hover( getByText(menuElement, TWO_DEPTHS_MENU_TEXTS.TOP_MENU_NON_SUB_MENU_ITEM, { ignore: HIDDEN_ELEMENT_SELECTOR }) ); - expect( - canvas.queryByText(TWO_DEPTHS_MENU_TEXTS.SUB_MENU_ITEM, { ignore: HIDDEN_ELEMENT_SELECTOR }) - ).not.toBeInTheDocument(); - expect( - canvas.queryByText(TWO_DEPTHS_MENU_TEXTS.SUB_SUB_MENU_ITEM, { ignore: HIDDEN_ELEMENT_SELECTOR }) - ).not.toBeInTheDocument(); + await waitFor(() => { + expect( + canvas.queryByText(TWO_DEPTHS_MENU_TEXTS.SUB_MENU_ITEM, { ignore: HIDDEN_ELEMENT_SELECTOR }) + ).not.toBeInTheDocument(); + expect( + canvas.queryByText(TWO_DEPTHS_MENU_TEXTS.SUB_SUB_MENU_ITEM, { ignore: HIDDEN_ELEMENT_SELECTOR }) + ).not.toBeInTheDocument(); + }); }; const showSubSubMenusWithKeyboard = async canvas => { @@ -86,14 +88,18 @@ const showSubSubMenusWithKeyboard = async canvas => { //close sub-sub-menu - using left arrow await pressNavigationKey(NavigationCommand.LEFT_ARROW); - expect( - canvas.queryByText(menuElement, TWO_DEPTHS_MENU_TEXTS.SUB_SUB_MENU_ITEM, { ignore: HIDDEN_ELEMENT_SELECTOR }) - ).not.toBeInTheDocument(); + await waitFor(() => { + expect( + canvas.queryByText(menuElement, TWO_DEPTHS_MENU_TEXTS.SUB_SUB_MENU_ITEM, { ignore: HIDDEN_ELEMENT_SELECTOR }) + ).not.toBeInTheDocument(); + }); expectActiveElementToHavePartialText(TWO_DEPTHS_MENU_TEXTS.SUB_MENU_ITEM); //close sub-menu - using escape await userEvent.keyboard("{escape}"); - expect(canvas.queryByText(menuElement, TWO_DEPTHS_MENU_TEXTS.SUB_MENU_ITEM)).not.toBeInTheDocument(); + await waitFor(() => { + expect(canvas.queryByText(menuElement, TWO_DEPTHS_MENU_TEXTS.SUB_MENU_ITEM)).not.toBeInTheDocument(); + }); expectActiveElementToHavePartialText(TWO_DEPTHS_MENU_TEXTS.TOP_MENU_SUB_MENU_ITEM); }; diff --git a/packages/style/src/mixins/_motion.scss b/packages/style/src/mixins/_motion.scss new file mode 100644 index 0000000000..19f598ba79 --- /dev/null +++ b/packages/style/src/mixins/_motion.scss @@ -0,0 +1,15 @@ +// Wrap any non-essential animation or movement transition in `motion-safe` +// so users with `prefers-reduced-motion: reduce` get a static experience. +// `motion-reduce` is for the rare case where you need an explicit alternative +// (e.g. swap a slide for an instant opacity change). +@mixin motion-safe { + @media (prefers-reduced-motion: no-preference) { + @content; + } +} + +@mixin motion-reduce { + @media (prefers-reduced-motion: reduce) { + @content; + } +} diff --git a/packages/style/src/mixins/index.scss b/packages/style/src/mixins/index.scss index 194c1c6243..fd0e412325 100644 --- a/packages/style/src/mixins/index.scss +++ b/packages/style/src/mixins/index.scss @@ -1,3 +1,4 @@ @import "common"; +@import "motion"; @import "states"; @import "typography";