iOS: Overflowing elements flash on drawer close
Description of the issue
- iOS only: Elements that exist below the height of the screen, and that overflow off the page horizontally, will flash when the drawer is closed
- Sometimes on opening the drawer with these elements in view, the entire page will flash, and scroll position may be moved
- Tested on Safari, Chrome, and Firefox with same results in each
- Seems only to happen on real devices: the simulator behaves as expected (iPhone 15 Pro Max, iOS 17.2)
- On Android (Galaxy S22, Android 14) and desktop web (Mac M2 OS 14.4.1), the elements behave as expected with no flash (although sometimes scroll position of the page is recalculated on drawer opening on Android)
Environment
- Remix 2.9.2
- vaul 0.9.1
- tested on: iPad (9th Gen) OS 17.4.1
Link to reproduction repository
Video Recording
https://github.com/ParkerMJones/vaul-flash-repro/assets/80937296/dab773e5-94c8-48ea-974e-a7623a07b17d
Facing the same issue on iPhone 11
Can confirm this, also happening on my PWA and Safari on Iphone 13. It also doesn't allow me to interact with buttons inside (remove, cancel), only Overlay. It also make a big layout shift on both opening and closing modal.
Like you said, only happening on real devices both in development and production, but doesn't affect desktop nor mobile device mode in DevTools.
I thought it has to do something with setting body position to fixed upon opening modals, but even with noBodyStyles it still occurs. The more I think about it probably it has to do something with locking the scroll behaviour because when I put modal prop to false, everything works fine but I don't think that should be the solution since it disables Overlay.
I hope fix comes soon, cuz Vaul is really big part of making my PWA UX native-like.
Facing the same issue on 13 Pro Max
facing same issue on some android and ios devices
I'm facing the same issue on an iOS device when using the Arc Search browser.
In addition to the open and onOpenChange props, I also added disablePreventScroll and noBodyStyles to the root and that helped.
<ResponsiveDialogContext.Provider value={{ open, setOpen }}>
<Drawer open={open} onOpenChange={setOpen} disablePreventScroll noBodyStyles>
{children}
</Drawer>
</ResponsiveDialogContext.Provider>
@landry-fairwinds worked for me. Thanks for sharing the solution.
In addition to the
openandonOpenChangeprops, I also addeddisablePreventScrollandnoBodyStylesto the root and that helped.<ResponsiveDialogContext.Provider value={{ open, setOpen }}> <Drawer open={open} onOpenChange={setOpen} disablePreventScroll noBodyStyles> {children} </Drawer> </ResponsiveDialogContext.Provider>
My bad, this actually fixes the problem. I think I mistaken it for another prop. Thanks!
This should be fixed in #341
@emilkowalski What should we adjust now, after the updates? I removed disablePreventScrolland noBodyStyles now the drawer scrolls to the top and has a very weird behavior. I also can't trigger nested drawers anymore.
@landry-fairwinds disablePreventScroll doesn't exist on drawer root.
Any idea how to fix this?
I'm currently still experiencing this issue where the drawer flashes while it's dismissing. My suspicion is that state changes that happen mid-dismissal are causing it to flash
I found a workaround, inspired by this section in the Radix Dialog docs:
/**
* Props for FilterDrawer component
*
* This component addresses a UI flash issue that occurs when state changes happen
* while the drawer is animating closed:
*
* Problem: When filter state (e.g., price, score) updates while a drawer is animating closed,
* the drawer content may flash briefly in incorrect positions, creating a jarring visual effect.
*
* Solution: Delay state changes until after drawer animation completes
* 1. First close the drawer: setOpen(false)
* 2. Set pendingAction to indicate what action should run after closing (APPLY or RESET)
* 3. Wait for animation to complete (using timeout matching animation duration)
* 4. Execute the appropriate action callback (applyAction or resetAction)
*
* Usage pattern in filter components:
* - Define resetAction and applyAction callbacks
* - On button click, set pendingAction and close the drawer
* - Let FilterDrawer handle the timing of the action execution
*/
interface FilterDrawerProps {
applyAction?: () => void;
children: React.ReactNode;
onOpenChange: (open: boolean) => void;
open: boolean;
pendingAction?: FilterAction | null;
resetAction?: () => void;
animationDuration?: number;
}
export function FilterDrawer({
children,
open,
onOpenChange,
pendingAction,
resetAction,
applyAction,
animationDuration = DRAWER_ANIMATION_DURATION + 1,
...props
}: FilterDrawerProps) {
const handleAnimationEnd = useCallback(() => {
if (pendingAction === FilterActions.RESET) {
resetAction?.();
} else if (pendingAction === FilterActions.APPLY) {
applyAction?.();
}
}, [pendingAction, resetAction, applyAction]);
useEffect(() => {
if (open) return;
setTimeout(() => {
handleAnimationEnd();
}, animationDuration);
// Intentionally ignore handleAnimationEnd
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, pendingAction, animationDuration]);
return (
<Drawer open={open} onOpenChange={onOpenChange} {...props}>
{children}
</Drawer>
);
}