Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/docs/docs/components/overlay/Tray/_mobileExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<VStack gap={2}>
<Button onPress={handleOpen}>Open Tray</Button>
{visible && (
<Tray reduceMotion onCloseComplete={handleClose} title="Reduced Motion">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we have customers request this prop? I do worry until we refresh motion and have built in support for reduced motion this will either not get used or will not be used consistently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't a customer request. It was discussed at our last onsite and this would be a meaningful addition for accessibility!

Regarding your concern for if this will be used: it's a good point to raise! In this PR there are docs site examples to call out the prop. Beyond that, I'm not sure how else we could enforce the usage of this prop.

This is more of a temporary solution since we should be offering this already. The current plan for our motion rework includes supporting animation configuration at the prop level. So this type of logic (determining animation based on props) will still exist after.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, makes sense!

<Text color="fgMuted">This tray fades in and out using opacity.</Text>
</Tray>
)}
</VStack>
);
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have this example match our others, maybe the basic one?

Such as

function ReducedMotionTray() {
  const [visible, setVisible] = useState(false);
  const { isPhone } = useBreakpoints();
  const handleOpen = () => setVisible(true);
  const handleClose = () => setVisible(false);

  return (
    <VStack gap={2}>
      <Button onClick={handleOpen}>Open Tray</Button>
      {visible && (
        <Tray
          reduceMotion
          pin={isPhone ? 'bottom' : 'right'}
          showHandleBar={isPhone}
          onCloseComplete={handleClose}
          title="Reduced Motion"
          footer={({ handleClose }) => (
            <PageFooter
              borderedTop
              justifyContent={isPhone ? 'center' : 'flex-end'}
              action={
                <Button block={isPhone} onClick={handleClose}>
                  Close
                </Button>
              }
            />
          )}
        >
          <Text paddingBottom={2}>
            This tray fades in and out using opacity.
          </Text>
        </Tray>
      )}
    </VStack>
  );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the example and updated the behavior of web Tray to prevent dragging behavior when reduceMotion === true


## Styling

### Handlebar
Expand Down
43 changes: 43 additions & 0 deletions apps/docs/docs/components/overlay/Tray/_webExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<VStack gap={2}>
<Button onClick={handleOpen}>Open Tray</Button>
{visible && (
<Tray
reduceMotion
pin={isPhone ? 'bottom' : 'right'}
showHandleBar={isPhone}
onCloseComplete={handleClose}
title="Reduced Motion"
footer={({ handleClose }) => (
<PageFooter
borderedTop
justifyContent={isPhone ? 'center' : 'flex-end'}
action={
<Button block={isPhone} onClick={handleClose}>
Close
</Button>
}
/>
)}
>
<Text paddingBottom={2}>This tray fades in and out using opacity.</Text>
</Tray>
)}
</VStack>
);
}
```

## 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`.
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile-app/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down Expand Up @@ -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: () =>
Expand Down
4 changes: 4 additions & 0 deletions packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.

<!-- template-start -->

## 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-common",
"version": "8.44.2",
"version": "8.45.0",
"description": "Coinbase Design System - Common",
"repository": {
"type": "git",
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp-server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.

<!-- template-start -->

## 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 6 additions & 0 deletions packages/mobile/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.

<!-- template-start -->

## 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
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mobile",
"version": "8.44.2",
"version": "8.45.0",
"description": "Coinbase Design System - Mobile",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';

import { Example, ExampleScreen } from '../../examples/ExampleScreen';

import { DefaultDrawer } from './Drawers';

const DrawerReduceMotionScreen = () => {
return (
<ExampleScreen>
<Example title="Reduce Motion Drawer">
<DefaultDrawer reduceMotion pin="bottom" />
</Example>
</ExampleScreen>
);
};

export default DrawerReduceMotionScreen;
7 changes: 5 additions & 2 deletions packages/mobile/src/overlays/__stories__/Drawers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import { Text } from '../../typography/Text';
import type { DrawerBaseProps } from '../drawer/Drawer';
import { Drawer } from '../drawer/Drawer';

export const DefaultDrawer = ({ pin = 'left' }: Pick<DrawerBaseProps, 'pin'>) => {
export const DefaultDrawer = ({
pin = 'left',
reduceMotion,
}: Pick<DrawerBaseProps, 'pin' | 'reduceMotion'>) => {
const [isVisible, setIsVisible] = useState(true);
const setIsVisibleOff = useCallback(() => setIsVisible(false), [setIsVisible]);
const setIsVisibleOn = useCallback(() => setIsVisible(true), [setIsVisible]);
return (
<>
<Button onPress={setIsVisibleOn}>Open</Button>
{isVisible && (
<Drawer onCloseComplete={setIsVisibleOff} pin={pin}>
<Drawer onCloseComplete={setIsVisibleOff} pin={pin} reduceMotion={reduceMotion}>
{({ handleClose }) => (
<VStack padding={2}>
<LoremIpsum />
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<ExampleScreen>
<Example title="Reduce Motion Tray">
<TrayWithReduceMotion />
</Example>
</ExampleScreen>
);
};

const TrayWithReduceMotion = () => {
const [isTrayVisible, setIsTrayVisible] = useState(false);
const setIsTrayVisibleOff = useCallback(() => setIsTrayVisible(false), [setIsTrayVisible]);
const setIsTrayVisibleOn = useCallback(() => setIsTrayVisible(true), [setIsTrayVisible]);
const trayRef = useRef<DrawerRefBaseProps>(null);

const handleTrayVisibilityChange = useCallback((e: 'visible' | 'hidden') => {
console.log('Tray visibility changed:', e);
}, []);

return (
<>
<Button onPress={setIsTrayVisibleOn}>Open</Button>
{isTrayVisible && (
<Tray
ref={trayRef}
reduceMotion
handleBarVariant="inside"
onCloseComplete={setIsTrayVisibleOff}
onVisibilityChange={handleTrayVisibilityChange}
title="Header"
>
<VStack paddingX={3}>
<Text font="body">
Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie,
interdum lorem id, viverra.
</Text>
</VStack>
</Tray>
)}
</>
);
};

export default TrayReduceMotionScreen;
12 changes: 10 additions & 2 deletions packages/mobile/src/overlays/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -140,6 +146,7 @@ export const Drawer = memo(
handleBarAccessibilityLabel = 'Dismiss',
accessibilityLabel,
accessibilityLabelledBy,
reduceMotion,
style,
styles,
accessibilityRole = 'alert',
Expand All @@ -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,
);
Expand Down Expand Up @@ -203,7 +211,7 @@ export const Drawer = memo(
const panGestureHandlers = useDrawerPanResponder({
pin,
drawerAnimation,
animateDrawerIn,
animateSnapBack,
disableCapturePanGestureToDismiss,
onBlur,
handleSwipeToClose,
Expand Down
26 changes: 26 additions & 0 deletions packages/mobile/src/overlays/drawer/__tests__/Drawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const MockDrawer = ({
onCloseComplete,
pin = 'bottom',
preventDismissGestures,
reduceMotion,
}: Partial<DrawerBaseProps>) => {
const [isVisible, setIsVisible] = useState(false);
const setIsVisibleOn = useCallback(() => setIsVisible(true), [setIsVisible]);
Expand All @@ -61,6 +62,7 @@ const MockDrawer = ({
onCloseComplete={handleRequestClose}
pin={pin}
preventDismissGestures={preventDismissGestures}
reduceMotion={reduceMotion}
visible={isVisible}
>
{({ handleClose }) => (
Expand Down Expand Up @@ -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(<MockDrawerWithSafeArea reduceMotion onCloseComplete={onCloseComplete} />);

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(<MockDrawerWithSafeArea reduceMotion onCloseComplete={onCloseComplete} />);

fireEvent.press(screen.getByText('Open Drawer'));
expect(screen.getByText(loremIpsum)).toBeTruthy();

fireEvent(screen.getByTestId('drawer-overlay'), 'onTouchStart');
await waitFor(() => expect(onCloseComplete).toHaveBeenCalledTimes(1));
});
});
});
Loading