diff --git a/apps/docs/docs/components/overlay/Tray/_mobileExamples.mdx b/apps/docs/docs/components/overlay/Tray/_mobileExamples.mdx
index fedf31c23..305625313 100644
--- a/apps/docs/docs/components/overlay/Tray/_mobileExamples.mdx
+++ b/apps/docs/docs/components/overlay/Tray/_mobileExamples.mdx
@@ -370,8 +370,33 @@ function PreventDismissTray() {
## Accessibility
+### Accessibility labels
+
Trays require an accessibility label. If you pass in a ReactNode to `title`, make sure to set `accessibilityLabel`.
+### Reduce Motion
+
+Use the `reduceMotion` prop to accommodate users with reduced motion settings.
+
+```jsx
+function ReducedMotionTray() {
+ const [visible, setVisible] = useState(false);
+ const handleOpen = () => setVisible(true);
+ const handleClose = () => setVisible(false);
+
+ return (
+
+
+ {visible && (
+
+ This tray fades in and out using opacity.
+
+ )}
+
+ );
+}
+```
+
## Styling
### Handlebar
diff --git a/apps/docs/docs/components/overlay/Tray/_webExamples.mdx b/apps/docs/docs/components/overlay/Tray/_webExamples.mdx
index 4e5298560..ac31fa252 100644
--- a/apps/docs/docs/components/overlay/Tray/_webExamples.mdx
+++ b/apps/docs/docs/components/overlay/Tray/_webExamples.mdx
@@ -441,8 +441,51 @@ function PreventDismissTray() {
## Accessibility
+### Accessibility labels
+
Trays require an accessibility label. If you pass in a ReactNode to `title`, make sure to set `accessibilityLabel` or `accessibilityLabelledBy`.
+### Reduce Motion
+
+Use the `reduceMotion` prop to accommodate users with reduced motion settings.
+
+```jsx live
+function ReducedMotionTray() {
+ const [visible, setVisible] = useState(false);
+ const { isPhone } = useBreakpoints();
+ const handleOpen = () => setVisible(true);
+ const handleClose = () => setVisible(false);
+
+ return (
+
+
+ {visible && (
+ (
+
+ Close
+
+ }
+ />
+ )}
+ >
+ This tray fades in and out using opacity.
+
+ )}
+
+ );
+}
+```
+
## Styling
The Tray component exposes `styles` and `classNames` props for customizing various parts of the component. Available keys include: `root`, `overlay`, `container`, `header`, `title`, `content`, `footer`, `handleBar`, `handleBarHandle`, and `closeButton`.
diff --git a/apps/mobile-app/src/routes.ts b/apps/mobile-app/src/routes.ts
index 20e857e39..45dc15491 100644
--- a/apps/mobile-app/src/routes.ts
+++ b/apps/mobile-app/src/routes.ts
@@ -246,6 +246,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/DrawerMisc.stories').default,
},
+ {
+ key: 'DrawerReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/DrawerReduceMotion.stories').default,
+ },
{
key: 'DrawerRight',
getComponent: () =>
@@ -840,6 +845,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/TrayRedesign.stories').default,
},
+ {
+ key: 'TrayReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/TrayReduceMotion.stories').default,
+ },
{
key: 'TrayScrollable',
getComponent: () =>
diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md
index 54367973c..714e7b004 100644
--- a/packages/common/CHANGELOG.md
+++ b/packages/common/CHANGELOG.md
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
+## 8.45.0 ((2/12/2026, 07:33 AM PST))
+
+This is an artificial version bump with no new change.
+
## 8.44.2 ((2/10/2026, 08:38 AM PST))
This is an artificial version bump with no new change.
diff --git a/packages/common/package.json b/packages/common/package.json
index 0763016ca..7a5977803 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-common",
- "version": "8.44.2",
+ "version": "8.45.0",
"description": "Coinbase Design System - Common",
"repository": {
"type": "git",
diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md
index 07d278cbe..e85c04073 100644
--- a/packages/mcp-server/CHANGELOG.md
+++ b/packages/mcp-server/CHANGELOG.md
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
+## 8.45.0 ((2/12/2026, 07:33 AM PST))
+
+This is an artificial version bump with no new change.
+
## 8.44.2 ((2/10/2026, 08:38 AM PST))
This is an artificial version bump with no new change.
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index 19878e938..184c80328 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mcp-server",
- "version": "8.44.2",
+ "version": "8.45.0",
"description": "Coinbase Design System - MCP Server",
"repository": {
"type": "git",
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index 8ea3b2404..e0caef6c7 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
+## 8.45.0 (2/12/2026 PST)
+
+#### 🚀 Updates
+
+- Add reduce motion support for Tray. [[#386](https://github.com/coinbase/cds/pull/386)]
+
## 8.44.2 (2/10/2026 PST)
#### 🐞 Fixes
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index e782e11fc..4e13eb603 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mobile",
- "version": "8.44.2",
+ "version": "8.45.0",
"description": "Coinbase Design System - Mobile",
"repository": {
"type": "git",
diff --git a/packages/mobile/src/overlays/__stories__/DrawerReduceMotion.stories.tsx b/packages/mobile/src/overlays/__stories__/DrawerReduceMotion.stories.tsx
new file mode 100644
index 000000000..28d4aa8a9
--- /dev/null
+++ b/packages/mobile/src/overlays/__stories__/DrawerReduceMotion.stories.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Example, ExampleScreen } from '../../examples/ExampleScreen';
+
+import { DefaultDrawer } from './Drawers';
+
+const DrawerReduceMotionScreen = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default DrawerReduceMotionScreen;
diff --git a/packages/mobile/src/overlays/__stories__/Drawers.tsx b/packages/mobile/src/overlays/__stories__/Drawers.tsx
index 21bcbf8c8..8bbf0840b 100644
--- a/packages/mobile/src/overlays/__stories__/Drawers.tsx
+++ b/packages/mobile/src/overlays/__stories__/Drawers.tsx
@@ -11,7 +11,10 @@ import { Text } from '../../typography/Text';
import type { DrawerBaseProps } from '../drawer/Drawer';
import { Drawer } from '../drawer/Drawer';
-export const DefaultDrawer = ({ pin = 'left' }: Pick) => {
+export const DefaultDrawer = ({
+ pin = 'left',
+ reduceMotion,
+}: Pick) => {
const [isVisible, setIsVisible] = useState(true);
const setIsVisibleOff = useCallback(() => setIsVisible(false), [setIsVisible]);
const setIsVisibleOn = useCallback(() => setIsVisible(true), [setIsVisible]);
@@ -19,7 +22,7 @@ export const DefaultDrawer = ({ pin = 'left' }: Pick) =>
<>
{isVisible && (
-
+
{({ handleClose }) => (
diff --git a/packages/mobile/src/overlays/__stories__/TrayReduceMotion.stories.tsx b/packages/mobile/src/overlays/__stories__/TrayReduceMotion.stories.tsx
new file mode 100644
index 000000000..2bdfbe40c
--- /dev/null
+++ b/packages/mobile/src/overlays/__stories__/TrayReduceMotion.stories.tsx
@@ -0,0 +1,54 @@
+import React, { useCallback, useRef, useState } from 'react';
+
+import { Button } from '../../buttons/Button';
+import { Example, ExampleScreen } from '../../examples/ExampleScreen';
+import { VStack } from '../../layout';
+import { Text } from '../../typography/Text';
+import type { DrawerRefBaseProps } from '../drawer/Drawer';
+import { Tray } from '../tray/Tray';
+
+export const TrayReduceMotionScreen = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+const TrayWithReduceMotion = () => {
+ const [isTrayVisible, setIsTrayVisible] = useState(false);
+ const setIsTrayVisibleOff = useCallback(() => setIsTrayVisible(false), [setIsTrayVisible]);
+ const setIsTrayVisibleOn = useCallback(() => setIsTrayVisible(true), [setIsTrayVisible]);
+ const trayRef = useRef(null);
+
+ const handleTrayVisibilityChange = useCallback((e: 'visible' | 'hidden') => {
+ console.log('Tray visibility changed:', e);
+ }, []);
+
+ return (
+ <>
+
+ {isTrayVisible && (
+
+
+
+ Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie,
+ interdum lorem id, viverra.
+
+
+
+ )}
+ >
+ );
+};
+
+export default TrayReduceMotionScreen;
diff --git a/packages/mobile/src/overlays/drawer/Drawer.tsx b/packages/mobile/src/overlays/drawer/Drawer.tsx
index 1bb5ba6f6..6dbfba09d 100644
--- a/packages/mobile/src/overlays/drawer/Drawer.tsx
+++ b/packages/mobile/src/overlays/drawer/Drawer.tsx
@@ -99,6 +99,12 @@ export type DrawerBaseProps = SharedProps &
* @deprecated Use TrayStickyFooter as a Tray child instead.
*/
stickyFooter?: DrawerRenderChildren | React.ReactNode;
+ /**
+ * When true, the drawer opens and closes with an opacity fade instead of
+ * a slide animation. Swipe-to-dismiss gestures remain enabled and use
+ * the slide transform so the drawer follows the user's finger naturally.
+ */
+ reduceMotion?: boolean;
};
export type DrawerProps = DrawerBaseProps & {
@@ -140,6 +146,7 @@ export const Drawer = memo(
handleBarAccessibilityLabel = 'Dismiss',
accessibilityLabel,
accessibilityLabelledBy,
+ reduceMotion,
style,
styles,
accessibilityRole = 'alert',
@@ -156,9 +163,10 @@ export const Drawer = memo(
drawerAnimation,
animateDrawerOut,
animateDrawerIn,
+ animateSnapBack,
drawerAnimationStyles,
animateSwipeToClose,
- } = useDrawerAnimation(pin, verticalDrawerPercentageOfView);
+ } = useDrawerAnimation(pin, verticalDrawerPercentageOfView, reduceMotion);
const [opacityAnimation, animateOverlayIn, animateOverlayOut] = useOverlayAnimation(
drawerAnimationDefaultDuration,
);
@@ -203,7 +211,7 @@ export const Drawer = memo(
const panGestureHandlers = useDrawerPanResponder({
pin,
drawerAnimation,
- animateDrawerIn,
+ animateSnapBack,
disableCapturePanGestureToDismiss,
onBlur,
handleSwipeToClose,
diff --git a/packages/mobile/src/overlays/drawer/__tests__/Drawer.test.tsx b/packages/mobile/src/overlays/drawer/__tests__/Drawer.test.tsx
index 349879927..5fded9f3b 100644
--- a/packages/mobile/src/overlays/drawer/__tests__/Drawer.test.tsx
+++ b/packages/mobile/src/overlays/drawer/__tests__/Drawer.test.tsx
@@ -42,6 +42,7 @@ const MockDrawer = ({
onCloseComplete,
pin = 'bottom',
preventDismissGestures,
+ reduceMotion,
}: Partial) => {
const [isVisible, setIsVisible] = useState(false);
const setIsVisibleOn = useCallback(() => setIsVisible(true), [setIsVisible]);
@@ -61,6 +62,7 @@ const MockDrawer = ({
onCloseComplete={handleRequestClose}
pin={pin}
preventDismissGestures={preventDismissGestures}
+ reduceMotion={reduceMotion}
visible={isVisible}
>
{({ handleClose }) => (
@@ -151,4 +153,28 @@ describe('Drawer', () => {
await delay(DURATION);
expect(onCloseComplete).not.toHaveBeenCalled();
});
+
+ describe('reduceMotion', () => {
+ it('closes the Drawer when the close button is pressed with reduceMotion enabled', async () => {
+ const onCloseComplete = jest.fn();
+ render();
+
+ fireEvent.press(screen.getByText('Open Drawer'));
+ expect(screen.getByText(loremIpsum)).toBeTruthy();
+
+ fireEvent.press(screen.getByText('Close Drawer'));
+ await waitFor(() => expect(onCloseComplete).toHaveBeenCalledTimes(1));
+ });
+
+ it('still closes the Drawer via overlay press with reduceMotion enabled', async () => {
+ const onCloseComplete = jest.fn();
+ render();
+
+ fireEvent.press(screen.getByText('Open Drawer'));
+ expect(screen.getByText(loremIpsum)).toBeTruthy();
+
+ fireEvent(screen.getByTestId('drawer-overlay'), 'onTouchStart');
+ await waitFor(() => expect(onCloseComplete).toHaveBeenCalledTimes(1));
+ });
+ });
});
diff --git a/packages/mobile/src/overlays/drawer/useDrawerAnimation.ts b/packages/mobile/src/overlays/drawer/useDrawerAnimation.ts
index 240e52820..f1756fe9b 100644
--- a/packages/mobile/src/overlays/drawer/useDrawerAnimation.ts
+++ b/packages/mobile/src/overlays/drawer/useDrawerAnimation.ts
@@ -24,6 +24,7 @@ const animateDrawer = {
export const useDrawerAnimation = (
pin: PinningDirection | undefined = 'bottom',
verticalDrawerPercentageOfView: number | undefined = defaultVerticalDrawerPercentageOfView,
+ reduceMotion?: boolean,
) => {
const windowDimensions = useWindowDimensions();
@@ -33,15 +34,40 @@ export const useDrawerAnimation = (
: windowDimensions.width * horizontalDrawerPercentageOfView;
const drawerAnimation = useRef(new Animated.Value(0));
+ // Separate opacity value used when reduceMotion is true so that
+ // open/close-button fades are independent of the transform that
+ // the pan-responder drives during swipe gestures.
+ const contentOpacity = useRef(new Animated.Value(reduceMotion ? 0 : 1));
- const animateDrawerIn = useMemo(
- () => Animated.timing(drawerAnimation.current, animateDrawer.animateIn),
- [],
- );
- const animateDrawerOut = useMemo(
- () => Animated.timing(drawerAnimation.current, animateDrawer.animateOut),
- [],
- );
+ const animateDrawerIn = useMemo(() => {
+ if (reduceMotion) {
+ return Animated.parallel([
+ Animated.timing(drawerAnimation.current, {
+ ...animateDrawer.animateIn,
+ duration: 0,
+ }),
+ Animated.timing(contentOpacity.current, animateDrawer.animateIn),
+ ]);
+ }
+ return Animated.timing(drawerAnimation.current, animateDrawer.animateIn);
+ }, [reduceMotion]);
+
+ const animateDrawerOut = useMemo(() => {
+ if (reduceMotion) {
+ return Animated.timing(contentOpacity.current, animateDrawer.animateOut);
+ }
+ return Animated.timing(drawerAnimation.current, animateDrawer.animateOut);
+ }, [reduceMotion]);
+
+ const animateSnapBack = useMemo(() => {
+ if (reduceMotion) {
+ return Animated.parallel([
+ Animated.timing(drawerAnimation.current, animateDrawer.animateIn),
+ Animated.timing(contentOpacity.current, animateDrawer.animateIn),
+ ]);
+ }
+ return Animated.timing(drawerAnimation.current, animateDrawer.animateIn);
+ }, [reduceMotion]);
/** custom animation config for swipe and fling to close that has no friction and is faster */
const animateSwipeToClose = useMemo(
@@ -89,13 +115,30 @@ export const useDrawerAnimation = (
}
}, [pin, drawerDimension]);
+ const drawerAnimationStyles = useMemo(() => {
+ if (reduceMotion) {
+ return {
+ opacity: contentOpacity.current,
+ transform: [translation],
+ };
+ }
+ return { transform: [translation] };
+ }, [reduceMotion, translation]);
+
return useMemo(() => {
return {
drawerAnimation: drawerAnimation.current,
animateDrawerOut,
animateDrawerIn,
- drawerAnimationStyles: { transform: [translation] },
+ animateSnapBack,
+ drawerAnimationStyles,
animateSwipeToClose,
};
- }, [animateDrawerOut, animateDrawerIn, translation, animateSwipeToClose]);
+ }, [
+ animateDrawerOut,
+ animateDrawerIn,
+ animateSnapBack,
+ drawerAnimationStyles,
+ animateSwipeToClose,
+ ]);
};
diff --git a/packages/mobile/src/overlays/drawer/useDrawerPanResponder.ts b/packages/mobile/src/overlays/drawer/useDrawerPanResponder.ts
index ea2c65be9..5323da494 100644
--- a/packages/mobile/src/overlays/drawer/useDrawerPanResponder.ts
+++ b/packages/mobile/src/overlays/drawer/useDrawerPanResponder.ts
@@ -19,7 +19,7 @@ import { modulate } from '@coinbase/cds-common/utils/modulate';
type UseDrawerPanResponderParams = {
drawerAnimation: Animated.Value;
- animateDrawerIn: Animated.CompositeAnimation;
+ animateSnapBack: Animated.CompositeAnimation;
pin: PinningDirection;
disableCapturePanGestureToDismiss: boolean;
onBlur?: () => void;
@@ -38,7 +38,7 @@ const calculateDragOffset = (x: number) => {
export const useDrawerPanResponder = ({
pin,
drawerAnimation,
- animateDrawerIn,
+ animateSnapBack,
disableCapturePanGestureToDismiss,
onBlur,
handleSwipeToClose,
@@ -233,13 +233,13 @@ export const useDrawerPanResponder = ({
onBlur?.();
handleSwipeToClose();
} else {
- animateDrawerIn.start();
+ animateSnapBack.start();
}
},
});
}, [
drawerAnimation,
- animateDrawerIn,
+ animateSnapBack,
parseGestureState,
shouldCaptureGestures,
shouldDismiss,
diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md
index 169a44b40..980fc6b98 100644
--- a/packages/web/CHANGELOG.md
+++ b/packages/web/CHANGELOG.md
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
+## 8.45.0 (2/12/2026 PST)
+
+#### 🚀 Updates
+
+- Add reduce motion support for Tray. [[#386](https://github.com/coinbase/cds/pull/386)]
+
## 8.44.2 (2/10/2026 PST)
#### 🐞 Fixes
diff --git a/packages/web/package.json b/packages/web/package.json
index 8455410ca..e9d6c629d 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-web",
- "version": "8.44.2",
+ "version": "8.45.0",
"description": "Coinbase Design System - Web",
"repository": {
"type": "git",
diff --git a/packages/web/src/overlays/tray/Tray.tsx b/packages/web/src/overlays/tray/Tray.tsx
index 5a087631b..c6941a463 100644
--- a/packages/web/src/overlays/tray/Tray.tsx
+++ b/packages/web/src/overlays/tray/Tray.tsx
@@ -18,7 +18,14 @@ import {
type OverlayContentContextValue,
} from '@coinbase/cds-common/overlays/OverlayContentContext';
import { css } from '@linaria/core';
-import { domMax, LazyMotion, m as motion, useAnimate, useDragControls } from 'framer-motion';
+import {
+ domMax,
+ LazyMotion,
+ m as motion,
+ MotionConfig,
+ useAnimate,
+ useDragControls,
+} from 'framer-motion';
import { IconButton } from '../../buttons';
import { cx } from '../../cx';
@@ -186,6 +193,11 @@ export type TrayBaseProps = {
* @default true
*/
restoreFocusOnUnmount?: boolean;
+ /**
+ * When true, the tray will use opacity animation instead of transform animation.
+ * This is useful for supporting reduced motion for accessibility.
+ */
+ reduceMotion?: boolean;
/**
* Sets an accessible label for the close button.
* On web, maps to `aria-label` and defines a string value that labels an interactive element.
@@ -254,6 +266,7 @@ export const Tray = memo(
accessibilityLabelledBy,
focusTabIndexElements,
restoreFocusOnUnmount = true,
+ reduceMotion,
closeAccessibilityLabel = 'Close',
closeAccessibilityHint,
styles,
@@ -294,18 +307,21 @@ export const Tray = memo(
const handleClose = useCallback(() => {
if (!scope.current) return;
- animate(
- scope.current,
- isSideTray
+
+ let finalAnimationValue;
+ if (reduceMotion) {
+ finalAnimationValue = { opacity: 0 };
+ } else {
+ finalAnimationValue = isSideTray
? { x: pin === 'right' ? '100%' : '-100%' }
- : { y: pin === 'bottom' ? '100%' : '-100%' },
- animationConfig.slideOut.transition,
- ).then(() => {
+ : { y: pin === 'bottom' ? '100%' : '-100%' };
+ }
+ animate(scope.current, finalAnimationValue, animationConfig.slideOut.transition).then(() => {
setIsOpen(false);
onClose?.();
onCloseComplete?.();
});
- }, [animate, scope, isSideTray, pin, onClose, onCloseComplete]);
+ }, [animate, scope, isSideTray, pin, onClose, onCloseComplete, reduceMotion]);
const handleSwipeClose = useCallback(() => {
if (!scope.current) return;
@@ -349,15 +365,21 @@ export const Tray = memo(
[trayHeight, handleSwipeClose, animate, scope],
);
- const initialAnimationValue = useMemo(
- () =>
- isSideTray
- ? { x: pin === 'right' ? '100%' : '-100%' }
- : { y: pin === 'bottom' ? '100%' : '-100%' },
- [isSideTray, pin],
- );
-
- const animateValue = useMemo(() => (isSideTray ? { x: 0 } : { y: 0 }), [isSideTray]);
+ const initialAnimationValue = useMemo(() => {
+ if (reduceMotion) {
+ return { opacity: 0 };
+ }
+ return isSideTray
+ ? { x: pin === 'right' ? '100%' : '-100%' }
+ : { y: pin === 'bottom' ? '100%' : '-100%' };
+ }, [isSideTray, pin, reduceMotion]);
+
+ const animateValue = useMemo(() => {
+ if (reduceMotion) {
+ return { opacity: 1 };
+ }
+ return isSideTray ? { x: 0 } : { y: 0 };
+ }, [isSideTray, reduceMotion]);
// Handle bar only shows for bottom-pinned trays (matching mobile behavior)
const shouldShowHandleBar = showHandleBar && pin === 'bottom';
@@ -427,160 +449,165 @@ export const Tray = memo(
style={styles?.overlay}
testID="tray-overlay"
/>
-
-
- ) => e.stopPropagation()}
- onDragEnd={!preventDismiss ? handleDragEnd : undefined}
- pin={pin}
- role={role}
- style={{
- maxHeight: isSideTray ? undefined : verticalDrawerPercentageOfView,
- touchAction: !preventDismiss && pin === 'bottom' ? 'none' : undefined,
- ...styles?.container,
- }}
- tabIndex={0}
- transition={animationConfig.slideIn.transition}
- width={isSideTray ? 'min(400px, 100vw)' : undefined}
+
+
+
-
- {(shouldShowTitle || headerContent || shouldShowHandleBar) && (
-
- {shouldShowHandleBar &&
- (preventDismiss ? (
-
- ) : (
- ) => {
- dragControls.start(e);
- }}
- styles={{
- root: styles?.handleBar,
- handle: { ...styles?.handleBarHandle, touchAction: 'none' },
- }}
- />
- ))}
- {shouldShowTitle && (
-
- {title &&
- (typeof title === 'string' ? (
-
- {title}
-
- ) : (
- title
- ))}
- {shouldShowCloseButton && (
-
- )}
-
- )}
- {headerContent}
-
+ ) => e.stopPropagation()}
+ onDragEnd={!preventDismiss ? handleDragEnd : undefined}
+ pin={pin}
+ role={role}
+ style={{
+ maxHeight: isSideTray ? undefined : verticalDrawerPercentageOfView,
+ touchAction: !preventDismiss && pin === 'bottom' ? 'none' : undefined,
+ ...styles?.container,
+ }}
+ tabIndex={0}
+ transition={animationConfig.slideIn.transition}
+ width={isSideTray ? 'min(400px, 100vw)' : undefined}
+ >
- {content}
+ {(shouldShowTitle || headerContent || shouldShowHandleBar) && (
+
+ {shouldShowHandleBar &&
+ (preventDismiss ? (
+
+ ) : (
+ ) => {
+ dragControls.start(e);
+ }}
+ styles={{
+ root: styles?.handleBar,
+ handle: { ...styles?.handleBarHandle, touchAction: 'none' },
+ }}
+ />
+ ))}
+ {shouldShowTitle && (
+
+ {title &&
+ (typeof title === 'string' ? (
+
+ {title}
+
+ ) : (
+ title
+ ))}
+ {shouldShowCloseButton && (
+
+ )}
+
+ )}
+ {headerContent}
+
+ )}
+
+ {content}
+
+ {footerContent}
- {footerContent}
-
-
-
-
+
+
+
+
diff --git a/packages/web/src/overlays/tray/__stories__/Tray.stories.tsx b/packages/web/src/overlays/tray/__stories__/Tray.stories.tsx
index 5d106cf67..980b4d70f 100644
--- a/packages/web/src/overlays/tray/__stories__/Tray.stories.tsx
+++ b/packages/web/src/overlays/tray/__stories__/Tray.stories.tsx
@@ -581,3 +581,32 @@ export const ResponsiveFullBleedImageListCells = () => {
>
);
};
+
+export const ReduceMotion = () => {
+ const [showBasicTray, setShowBasicTray] = useState(false);
+
+ return (
+
+ Basic Tray with String Title
+
+ {showBasicTray && (
+ setShowBasicTray(false)}
+ title="Basic Tray Example"
+ >
+
+
+ This is a basic tray with a simple string title. Clicking outside or pressing ESC will
+ close it.
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod, nisl eget
+ aliquam ultricies, nunc nisl aliquet nunc, quis aliquam nunc nisl eu nunc.
+
+
+
+ )}
+
+ );
+};
diff --git a/packages/web/src/overlays/tray/__tests__/Tray.test.tsx b/packages/web/src/overlays/tray/__tests__/Tray.test.tsx
index 1b9dc3c27..b41c6f75f 100644
--- a/packages/web/src/overlays/tray/__tests__/Tray.test.tsx
+++ b/packages/web/src/overlays/tray/__tests__/Tray.test.tsx
@@ -402,6 +402,23 @@ describe('Tray', () => {
expect(tray).toHaveAttribute('aria-labelledby', LABELLED_BY);
expect(tray).toHaveAttribute('aria-label', LABEL);
});
+
+ it('uses opacity animation when reduceMotion is true', async () => {
+ const onCloseCompleteSpy = jest.fn();
+ render(
+
+
+ {loremIpsum}
+
+ ,
+ );
+
+ // Test "closed" opacity style by asserting the opacity style asynchronously before the animation completes
+ expect(screen.getByTestId('tray')).toHaveStyle({ opacity: 0 });
+ await waitFor(() => {
+ expect(screen.getByTestId('tray')).toHaveStyle({ opacity: 1 });
+ });
+ });
});
describe('static classNames', () => {