Skip to content

feat: Add reduce motion support to <Tray>#386

Merged
maximo-macchi-cb merged 14 commits intomasterfrom
max/add-reduce-motion-to-drawer
Feb 12, 2026
Merged

feat: Add reduce motion support to <Tray>#386
maximo-macchi-cb merged 14 commits intomasterfrom
max/add-reduce-motion-to-drawer

Conversation

@maximo-macchi-cb
Copy link
Contributor

What changed? Why?

This PR adds support to reduce the motion of the <Tray> components via a new prop: reduceMotion. When enabled, the tray fades in / out using opacity styles versus transforms. This gives users the ability to switch the component's animation so that it accommodates users with reduced motion accessibility settings enabled.

Note that the mobile implementation of <Tray> is different since it uses React Native's Animated library. To achieve the reduced motion styles, both <Drawer> and <Tray> had to be updated. <Tray> just drills the reduceMotion prop into <Drawer>.

Root cause (required for bugfixes)

UI changes (only net new changes)

iOS

Screen.Recording.2026-02-10.at.3.19.50.PM.mov

Web

Screen.Recording.2026-02-10.at.3.15.55.PM.mov

Testing

How has it been tested?

  • Unit tests
  • Interaction tests
  • Pseudo State tests
  • Manual - Web
  • Manual - Android (Emulator / Device)
  • Manual - iOS (Emulator / Device)

Testing instructions

Illustrations/Icons Checklist

Required if this PR changes files under packages/illustrations/** or packages/icons/**

  • verified visreg changes with Terran (include link to visreg run/approval)
  • all illustration/icons names have been reviewed by Dom and/or Terran

Change management

type=routine
risk=low
impact=sev5

automerge=false

@maximo-macchi-cb maximo-macchi-cb self-assigned this Feb 10, 2026
@cb-heimdall
Copy link
Collaborator

cb-heimdall commented Feb 10, 2026

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 1
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1
CODEOWNERS ✅ See below

CODEOWNERS

Code Owner Status Calculation
ui-systems-eng-team 1/1
Denominator calculation
Additional CODEOWNERS Requirement
Show calculation
Sum 0
0
From CODEOWNERS 1
Sum 1

Copy link
Contributor

@sverg-cb sverg-cb left a comment

Choose a reason for hiding this comment

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

Do we leave it to the devs using this to set the flag themselves? I like the flexibility, but would it always render w/ motion even if the user's OS has motion reduced unless the dev handles that on their end?

@maximo-macchi-cb
Copy link
Contributor Author

Do we leave it to the devs using this to set the flag themselves? I like the flexibility, but would it always render w/ motion even if the user's OS has motion reduced unless the dev handles that on their end?

Great question! It's the customer's responsibility to configure the prop since the customer will have more context on how a11y settings are configured in their app. CDS as a library should not assume the mechanism for how reduced motion settings are set. Apps using CDS may have their own internal a11y settings which include motion or they may rely on the motion settings at the OS level.

<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!

</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

/**
* When true, replaces the slide animation with an opacity fade and disables
* swipe-to-dismiss pan gestures. Use this to support reduced motion for accessibility.
* @default false
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can you remove @default false

// MAX_OVER_DRAG padding accommodates the over-drag area during swipe gestures.
// It is normally hidden off-screen by the slide transform. When reduceMotion is
// true there is no transform, so the extra padding must be excluded.
const overDragPadding = reduceMotion ? 0 : MAX_OVER_DRAG;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this needed? Could we still have this padding when reduced motion is enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The padding is only needed when a user can drag to close the tray. Since a user can drag the tray up by MAX_OVER_DRAG pixels, the pixels are added and the translate animation moves the tray down so the extra padding is hidden. With reduced motion enabled, a user cannot drag the tray so the extra pixels aren't needed. And there's no translate animation to hide the extra padding.

This is what a tray with reduced motion looks like if we kept the MAX_OVER_DRAG padding always added.

Screenshot 2026-02-11 at 12 28 42 PM

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we keep the ability to drag to close the tray? The user is deciding to swipe in that case.

I turned on reduced motion on my phone and the apps I tried still allowed me to drag to close. With reduced motion on but prefer cross-fade transitions off it still did the enter slide up animation as well

Copy link
Contributor

@hcopp hcopp Feb 11, 2026

Choose a reason for hiding this comment

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

trim.BBE5F206-3D00-4B60-BC00-9223208216A0.MOV

Hopefully this isn't too hard to see. I'm curious what other apps do as well

Besides this, I don't think useDrawerSpacing needs to be made aware of reduceMotion. It sounds like cases with preventDismiss which also should disable drag would want this feature as well, right?

Appreciate you looking into this and for willing to do all the back and forth!

Copy link
Contributor Author

@maximo-macchi-cb maximo-macchi-cb Feb 11, 2026

Choose a reason for hiding this comment

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

Good point with allowing drag regardless of reduceMotion being true or not.

Even if other apps seem to show the translate animation with reduced motion on, I still advise we disable swipe-ability all together. Accidental swipe gestures could happen. And if a user has reduced motion enabled on their device, I'd argue it's a reasonable expectation for users that there won't be any advanced animations occurring. I checked Material UI, Shopify, and Atlassian but didn't see swipe-ability in their drawer components. At least on web.

As for how this relates to a device's a11y settings, Stephen asked the same thing here so it's good to think about! TLDR is it should be the customer's responsibility to configure reduced motion.

useDrawerSpacing might still need reduceMotion. Even if preventDismiss === true, the translate animation still occurs which hides the extra padding. This would require refactoring useDrawerSpacing more thoroughly and I figured bringing in the reduceMotion prop was a simpler solution. Do you think there's a benefit to omitting reduceMotion from the hook?

Copy link
Contributor

Choose a reason for hiding this comment

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

Even if other apps seem to show the translate animation with reduced motion on, I still advise we disable swipe-ability all together. Accidental swipe gestures could happen. And if a user has reduced motion enabled on their device, I'd argue it's a reasonable expectation for users that there won't be any advanced animations occurring. I checked Material UI, Shopify, and Atlassian but didn't see swipe-ability in their drawer components. At least on web.

I think we will need to check mobile apps that are known for good a11y compliance. If we are the only ones that disable swipe when reduce motion is on that would be very confusing to me as a consumer.

useDrawerSpacing might still need reduceMotion. Even if preventDismiss === true, the translate animation still occurs which hides the extra padding. This would require refactoring useDrawerSpacing more thoroughly and I figured bringing in the reduceMotion prop was a simpler solution. Do you think there's a benefit to omitting reduceMotion from the hook?

It makes sense for Drawer and Tray to have reduceMotion since they are dealing with the animation. useDrawerSpacing doesn't have any direct knowledge of this. If in the future we decided to get rid of the translate animation in other situations we would need another prop or we would piggyback on reduceMotion.

Two paths that make sense to me

  1. Expose a prop for max drag on useDrawerSpacing and set it to MAX_OVER_DRAG by default
  2. Have an animate type prop which dynamically decides whether to factor in MAX_OVER_DRAG

Besides this, it does feel a little weird that we have this max over drag at all, I wonder if we could even get rid of this (unrelated to this PR but adding a prop sorta locks us in).

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 made some updates to allow the tray to be dragged to close on mobile. This also removes the issue of reduceMotion being in useDrawerSpacing since no changes are needed there. Since swipe-ability will always be turned on, the MAX_OVER_DRAG padding will always need to be applied

hcopp
hcopp previously approved these changes Feb 12, 2026
Copy link
Contributor

@hcopp hcopp left a comment

Choose a reason for hiding this comment

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

Code changes look great! Thanks for all the back and forth, I can give it another stamp once you do versioning

@maximo-macchi-cb maximo-macchi-cb merged commit a592731 into master Feb 12, 2026
28 checks passed
@maximo-macchi-cb maximo-macchi-cb deleted the max/add-reduce-motion-to-drawer branch February 12, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants