feat: Add reduce motion support to <Tray>#386
Conversation
✅ Heimdall Review Status
✅
|
| Code Owner | Status | Calculation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ui-systems-eng-team |
✅
1/1
|
Denominator calculation
|
sverg-cb
left a comment
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| </VStack> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
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>
);
}
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Is this needed? Could we still have this padding when reduced motion is enabled?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
- Expose a prop for max drag on useDrawerSpacing and set it to MAX_OVER_DRAG by default
- 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).
There was a problem hiding this comment.
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
1bb7d50 to
8bb6e09
Compare
hcopp
left a comment
There was a problem hiding this comment.
Code changes look great! Thanks for all the back and forth, I can give it another stamp once you do versioning
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 thereduceMotionprop 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?
Testing instructions
Illustrations/Icons Checklist
Required if this PR changes files under
packages/illustrations/**orpackages/icons/**Change management
type=routine
risk=low
impact=sev5
automerge=false