Skip to content

[$175] Web - App review modal opens without the dimmed backdrop #104039

Open

Description

@MelvinBot

Problem

On web staging, the "Are you enjoying New Expensify?" app review modal opens without the dimmed backdrop. Other confirm modals show the dimmed backdrop. This modal uses the same CONFIRM modal type, so it should look the same.

Reproduction steps

This has not been reproduced on demand yet.

  1. Sign in to staging on web with an account that qualifies for the app review prompt. Your account qualifies when nvp_appReview has a trigger, response isn't positive, and lastPrompt is empty or more than 30 days old (useProactiveAppReview.ts:29-46).
  2. Reload the app so the modal opens during startup.
  3. Look at the area behind the modal.

To test locally, set nvp_appReview in Onyx to {trigger: 'test'} with no lastPrompt or response, then reload.

Expected result

A dimmed backdrop covers the page behind the modal, like other confirm modals.

Actual result

The modal card shows, but the page behind it isn't dimmed.

Possible cause (unconfirmed)

  • The web backdrop fades in with a Reanimated Keyframe (Backdrop/index.web.tsx:28-34). Reanimated keeps the element hidden until the keyframe animation starts.
  • On web, the animation lasts only 1 ms (animationTiming/index.ts:3). Reanimated may remove the generated keyframe before the animation starts if the main thread is busy.
  • This modal opens on its own during startup, through lazy-loaded global modals (GlobalModals.tsx:32-43). The main thread is busiest then. Most other modals open after a click, when the page is idle.
  • The modal card still shows because it fades in a different way (Container/index.web.tsx:51-52).

To confirm, inspect the backdrop element in DevTools. If it has visibility: hidden and an animation-name that isn't defined in the page's styles, this is the cause.

Platforms

Reported on web (staging). Other platforms not checked yet.

Slack thread: https://expensify.slack.com/archives/C05LX9D6E07/p1791888055462809

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @nkdengineer

Activity

  1. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 14, 2026
  2. melvin-bot commented on Oct 14, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @parasharrajat (External)

  3. changed the title [-]Web - App review modal opens without the dimmed backdrop[/-] [+][$175] Web - App review modal opens without the dimmed backdrop[/+] on Oct 14, 2026
  4. melvin-bot commented on Oct 14, 2026

    @melvin-bot
  5. janiya2 commented on Oct 14, 2026

    @janiya2

    Contributor details
    Your Expensify account email: janithad2@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/janithad

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Web, the "Are you enjoying New Expensify?" app review modal (and other modals triggered during startup) opens without the dimmed backdrop, leaving the background undimmed. Meanwhile, confirm modals triggered by direct user interaction show the dimmed backdrop properly.


    What is the root cause of that problem?

    In src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx:28-34, the backdrop's entering animation is implemented using a Reanimated Keyframe:

    const Entering = useMemo(() => {
        if (!backdropOpacity) {
            return;
        }
        const FadeIn = new Keyframe(getModalInAnimation('fadeIn', backdropOpacity));
        return FadeIn.duration(animationInTiming).reduceMotion(ReduceMotion.Never);
    }, [animationInTiming, backdropOpacity]);

    and attached via entering={Entering} on Animated.View:

    <Animated.View
        style={[styles.modalBackdrop, backdropStyle, style]}
        entering={Entering}
        exiting={Exiting}
    />

    Here is the exact sequence causing the bug on Web:

    1. In react-native-reanimated web implementation, mounting an Animated.View with an entering Keyframe dynamically generates a CSS @keyframes rule, injects it into a document <style> tag, and assigns inline styles animation-name: ... and visibility: hidden to the DOM element until the CSS animation begins.
    2. In Expensify on Web, animationInTiming defaults to DEFAULT_IN = 1 ms (src/libs/Animation/animationTiming/index.ts:3).
    3. The app review modal is not triggered by a user click; it opens automatically during startup via useProactiveAppReview.ts inside GlobalModals.tsx:32-43 (deferred through requestIdleCallback with startTransition).
    4. During app startup, the browser's main thread is heavily occupied with Onyx storage reconciliation, font loading, component hydration, and lazy chunk evaluations.
    5. Because the animation duration is only 1ms and the main thread is busy, the CSS animation lifecycle fails to resolve cleanly—the animationstart event is either desynchronized or the temporary keyframe rule is not evaluated in time by the browser engine. As a result, the backdrop element gets permanently stuck with the inline style visibility: hidden, leaving the backdrop invisible.
    6. Crucially, Container/index.web.tsx:52-53 already encountered and solved this exact Reanimated web keyframe flaw in PR Fix test drive modal not moving when keyboard openes #62799:
    // instead of an entering transition since keyframe animations break keyboard on mWeb Chrome (#62799)
    const animatedStyles = useAnimatedStyle(() => getModalInAnimationStyle(animationIn)(initProgress.get()), [initProgress]);

    Backdrop/index.web.tsx, however, was never updated to follow this fix and was left with the fragile Keyframe entering animation.


    What changes do you think we should make in order to solve the problem?

    Align src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx with the established, battle-tested pattern in Container/index.web.tsx: replace the Keyframe entering animation with useAnimatedStyle and withTiming.

    Specifically, in src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx:

    1. Update imports from react-native-reanimated:
    import Animated, {Keyframe, ReduceMotion, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
    1. Replace the Entering Keyframe with an initProgress shared value driven by withTiming:
    function Backdrop({
        style,
        customBackdrop,
        onBackdropPress,
        animationInTiming = CONST.MODAL.ANIMATION_TIMING.DEFAULT_IN,
        animationOutTiming = CONST.MODAL.ANIMATION_TIMING.DEFAULT_OUT,
        isBackdropVisible,
        backdropOpacity = variables.overlayOpacity,
    }: BackdropProps) {
        const styles = useThemeStyles();
        const {translate} = useLocalize();
    
        const isInitiated = useSharedValue(false);
        const initProgress = useSharedValue(0);
    
        useEffect(() => {
            if (isInitiated.get()) {
                return;
            }
            isInitiated.set(true);
            initProgress.set(
                withTiming(1, {
                    duration: animationInTiming,
                    reduceMotion: ReduceMotion.Never,
                }),
            );
        }, [animationInTiming, initProgress, isInitiated]);
    
        const animatedBackdropStyle = useAnimatedStyle(
            () => ({
                opacity: initProgress.get() * backdropOpacity,
            }),
            [initProgress, backdropOpacity],
        );
    1. Update the Animated.View elements to remove entering={Entering} and apply animatedBackdropStyle:
        if (!customBackdrop) {
            return (
                <PressableWithoutFeedback
                    accessible
                    accessibilityLabel={translate('modal.backdropLabel')}
                    onPress={onBackdropPress}
                    style={[styles.userSelectNone, styles.cursorAuto]}
                    dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
                    sentryLabel={CONST.SENTRY_LABEL.REANIMATED_MODAL.BACKDROP}
                >
                    {isBackdropVisible && (
                        <Animated.View
                            style={[styles.modalBackdrop, animatedBackdropStyle, style]}
                            exiting={Exiting}
                        />
                    )}
                </PressableWithoutFeedback>
            );
        }
        return (
            isBackdropVisible && (
                <View
                    style={[styles.userSelectNone]}
                    dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
                >
                    <Animated.View
                        exiting={Exiting}
                        style={[styles.modalBackdrop, animatedBackdropStyle, style]}
                    >
                        {!!customBackdrop && customBackdrop}
                    </Animated.View>
                </View>
            )
        );

    Why this is the superior solution:

    • It completely eliminates Reanimated's CSS @keyframes insertion and the stuck inline visibility: hidden bug on web.
    • It is 100% consistent with the architectural precedent in Container/index.web.tsx (Fix test drive modal not moving when keyboard openes #62799).
    • The backdrop is guaranteed to render with full opacity even when mounting under high main-thread CPU pressure during app startup or idle callbacks.

    What alternative solutions did you explore? (Optional)

    1. Increasing animationInTiming to 300ms on Web: This would introduce an unwanted 300ms visual delay on Web modals, which contradicts Expensify's standard design of instant (1ms) web modal presentation, and still does not guarantee safety if main-thread lag exceeds 300ms.
    2. Pure CSS transitions: While functional, using useAnimatedStyle with withTiming is standard in Expensify's ReanimatedModal pipeline and maintains clean exit animation handling (exiting={Exiting}) and dynamic backdropOpacity support.

    Unit / Manual Verification Steps

    1. In DevTools Console or Onyx devtools, set the app review NVP:
    Onyx.set('nvp_appReview', {trigger: 'test'});
    1. Reload the page on Web.
    2. Verify that the "Are you enjoying New Expensify?" review modal opens on startup with the dimmed backdrop clearly covering the background.
    3. Verify that inspecting the backdrop element in DevTools shows opacity: 0.72 and does not have visibility: hidden.
    4. Open standard modals (e.g. Settings > Sign Out confirmation, or Delete Expense confirmation) and verify their backdrops still display and fade out smoothly.
  6. melvin-bot commented on Oct 14, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  7. taieddie commented on Oct 14, 2026

    @taieddie

    Contributor details
    Your Expensify account email: taieddie8@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~01af851c9a7f771775

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When the "Are you enjoying New Expensify?" app review modal (ProactiveAppReviewModal) opens automatically on Web, the modal card appears without its dimmed backdrop.

    What is the root cause of that problem?

    On Web, ReanimatedModal renders the modal card via src/components/Modal/ReanimatedModal/Container/index.web.tsx and the backdrop via src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx, but the two components use completely different animation mechanisms:

    1. In Container/index.web.tsx (lines 25-53, PR #62799), the modal card's entrance animation was explicitly migrated away from a Reanimated entering Keyframe to a shared value (initProgress = useSharedValue(0)) animated by withTiming(1, {duration: animationInTiming, easing, reduceMotion: ReduceMotion.Never}) and rendered via useAnimatedStyle. As noted by the inline comment on line 52: Web implementation triggers reanimated's progress progression manually cleanly instead of entering prop. Because withTiming updates inline opacity directly without setting visibility: hidden, the modal card always renders reliably.
    2. In Backdrop/index.web.tsx (lines 28-34, 64, and 78), the backdrop was never migrated to match Container/index.web.tsx and still relies on an entering Keyframe (new Keyframe(getModalInAnimation('fadeIn', backdropOpacity)).duration(animationInTiming).reduceMotion(ReduceMotion.Never)).
    3. On Web, Reanimated's Keyframe entering animation initializes the DOM element with visibility: hidden until the CSS animation start event fires to restore visibility: initial. As noted by the issue reporter, on Web animationInTiming defaults to CONST.MODAL.ANIMATION_TIMING.DEFAULT_IN = 1 ms (src/libs/Animation/animationTiming/index.ts), which is passed to Backdrop when no custom duration is provided. With a 1ms duration or when main-thread activity during startup delays the browser paint cycle, the keyframe animation finishes or is cleaned up before animationstart can fire. Because of this, visibility: initial is never restored, leaving the backdrop DOM element stuck at visibility: hidden behind the visible container.

    What changes do you think we should make in order to solve the problem?

    Bring src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx into full architectural alignment with src/components/Modal/ReanimatedModal/Container/index.web.tsx:

    1. In src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx:
      • Replace the Entering Keyframe memo with a shared value: const initProgress = useSharedValue(0).
    2. Add a useEffect watching isBackdropVisible, backdropOpacity, and animationInTiming:
      • When isBackdropVisible becomes true and backdropOpacity > 0:
        • Reset initProgress.set(0) so that repeated opens or visibility toggles always trigger a fresh fade-in.
        • Animate initProgress.set(withTiming(1, {duration: animationInTiming, easing, reduceMotion: ReduceMotion.Never})).
      • When isBackdropVisible becomes false, allow exiting={Exiting} (new Keyframe(getModalOutAnimation('fadeOut', backdropOpacity))) to handle the exit animation smoothly down to 0, exactly matching Container/index.web.tsx lines 55-69.
    3. Replace the static backdropStyle memo with an animated style:
      • const animatedStyles = useAnimatedStyle(() => ({opacity: initProgress.get() * backdropOpacity}), [initProgress, backdropOpacity]).
    4. Apply animatedStyles to <Animated.View style={[styles.modalBackdrop, backdropStyle, style, animatedStyles]} exiting={Exiting} /> in both the default and customBackdrop branches, and remove the entering={Entering} prop.

    What alternative solutions did you explore? (Optional)

    1. Increasing the Keyframe duration (e.g. Enforcing a minimum duration of 120ms or 200ms instead of 1ms on Web): While this reduces the likelihood of the 1ms race condition, Reanimated Keyframe entering animations on Web can still suffer from keyframe injection and visibility-lock issues under main-thread contention.
    2. Migrating Backdrop to useSharedValue and withTiming: This is the preferred solution because it completely eliminates the Keyframe visibility: hidden vulnerability on Web and matches the existing pattern in Container/index.web.tsx (PR #62799).

    Test Plan

    Preconditions:

    • Sign in on Web.
    • Open the browser DevTools console and set nvp_appReview in Onyx to trigger the modal: Onyx.set('nvp_appReview', {trigger: 'test'}).

    Verification Steps:

    1. Reload the page on Web.
    2. Verify that the "Are you enjoying New Expensify?" review modal opens and displays the dimmed backdrop behind the modal card.
    3. Inspect the backdrop element in DOM DevTools and verify that opacity animates smoothly to 0.72 and visibility is not hidden.
    4. Click on the backdrop and verify that the modal and backdrop fade out smoothly.

    Regression Verification:

    • Open other modals across Web (e.g. Delete expense confirm modal, Workspace settings, Right-Hand Panel sheets) and verify that the backdrop fades in and out smoothly with no visual flickering or regression.
  8. melvin-bot commented on Oct 14, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  9. khizzcr7 commented on Oct 15, 2026

    @khizzcr7

    Proposal

    What is the root cause of the problem?

    On web, the modal backdrop's entering animation is a Reanimated Keyframe attached via entering={Entering} in src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx. Reanimated's web implementation injects a CSS @keyframes rule and sets the element to visibility: hidden until the animation starts. The app-review modal opens automatically at startup (deferred through requestIdleCallback in useProactiveAppReview), when the main thread is saturated with Onyx reconciliation, font loading, and lazy chunk evaluation - and the animation duration is only 1ms (DEFAULT_IN). The CSS animation lifecycle never resolves cleanly under that load, so the backdrop element stays stuck at visibility: hidden and the background never dims. Modals opened by direct user interaction don't hit this because the main thread is idle. Notably, Container/index.web.tsx already fixed this exact Reanimated-web keyframe flaw (for the test-drive modal keyboard issue) by switching to useAnimatedStyle - the backdrop was never updated to match.

    How do you propose to fix it?

    Align Backdrop/index.web.tsx with the proven pattern in Container/index.web.tsx: replace the Keyframe entering animation with an initProgress shared value driven by withTiming(animationInTiming), and apply opacity through useAnimatedStyle (opacity: initProgress * backdropOpacity). Keep the existing exiting animation and backdropOpacity support, and remove the entering prop from the Animated.View. This eliminates the CSS keyframe injection entirely, so there is no visibility: hidden state to get stuck in.

    What will you do to verify the fix works?

    1. Reproduce via Onyx devtools: set nvp_appReview to {trigger: 'test'}, reload web, and confirm the review modal opens on startup WITH the dimmed backdrop (DevTools shows the backdrop at full opacity, no visibility: hidden).
    2. Open standard user-triggered modals (sign-out confirm, delete expense confirm) and confirm their backdrops still render and fade out correctly.
    3. Run the modal-related unit tests to confirm no regression.

    Contributor details

    Your Expensify account email: khizar.akram.ka@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~016c4e09f2cf8b7d6d

  10. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  11. kks0488 commented on Oct 16, 2026

    @kks0488

    Reproduced on web (main @ bf2fb24, Chrome).

    Steps

    1. Set nvp_appReview to {trigger: 'test'} in Onyx so the "Enjoying New Expensify?" modal opens on load.
    2. Reload while animation frames are delayed by ~800 ms (stands in for a busy startup or a background tab, where requestAnimationFrame is paused but timers keep running).

    Result

    • Normal reload: backdrop has visibility: visible, opacity: 0.72, page is dimmed.
    • Delayed frames: the backdrop is still mounted with opacity: 0.72, but its computed visibility stays hidden and its REA-ENTERING-* @keyframes rule is already gone from the stylesheets, so the card shows with no dim — same as the video in the OP.

    Note: on desktop web this modal animates with CENTERED_DURATION_IN_WEB = 120 ms (computed animation-duration: 0.12s on the live element), not 1 ms. Plain CPU throttling (6x/20x) did not reproduce it; it depends on frame timing vs. the timer that removes the keyframe rule.

    Happy to post a full proposal once this is ready for one.

  12. 1 remaining item

  13. kks0488 commented on Oct 16, 2026

    @kks0488

    Contributor details
    Your Expensify account email: kks0488@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0197507708d4b35a03

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On web, the "Enjoying New Expensify?" modal sometimes opens with no dim behind it.

    What is the root cause of that problem?

    I reproduced this on main (bf2fb24). The cause is how Reanimated runs the backdrop's entering Keyframe on web (Backdrop/index.web.tsx#L28-L34, used at L62-L66). In react-native-reanimated 4.7.0:

    1. A view with entering is rendered with visibility: hidden (createAnimatedComponent/AnimatedComponent.tsx L409-L419). Only the animationstart event makes it visible again (layoutReanimation/web/componentUtils.ts L268-L272).
    2. The animation-name is applied inside requestAnimationFrame (componentUtils.ts L213).
    3. A custom Keyframe's @keyframes rule is deleted by a plain setTimeout after max(duration * 5, ~160ms) (componentUtils.ts L276-L277, domUtils.ts L121-L141).

    On desktop web this modal uses CENTERED_DURATION_IN_WEB = 120 ms, not 1 ms (Modal/index.tsx#L49, CONST#L2348). I confirmed animation-duration: 0.12s on the live element. So the rule is deleted about 600 ms after the backdrop mounts.

    If the next animation frame comes later than that, animation-name points at a rule that no longer exists. That happens during a busy startup, which is when this modal auto-opens. It also happens in a background tab, where requestAnimationFrame is paused but timers still run. In that case animationstart never fires and the backdrop stays visibility: hidden until the modal closes. The modal card is fine because Container/index.web.tsx already uses a shared value instead of entering.

    Evidence (set nvp_appReview to {trigger: 'test'} in Onyx, then reload):

    • Normal reload: the backdrop element has visibility: visible and opacity: 0.72, and the page is dimmed.
    • Same steps, with animation frames delayed by 800 ms to stand in for a busy or background tab: the backdrop element is still mounted with opacity: 0.72. But its computed visibility is hidden, and its REA-ENTERING-* @keyframes rule is no longer in any stylesheet. Result: the card shows with no dim, which matches the report.

    What changes do you think we should make in order to solve the problem?

    Don't use a Reanimated layout animation for the web backdrop's fade-in. Fade it in with a plain CSS opacity transition instead. Nothing then hides the element, and nothing depends on a rule that a timer deletes.

    1. In Backdrop/index.web.tsx, remove entering={Entering} from both Animated.Views.
    2. Render the backdrop with opacity: 0 and transition: opacity <animationInTiming>ms using the same easing curve. Right after mount (in an effect, not requestAnimationFrame), switch the opacity to backdropOpacity. The browser then runs the fade on its own. If frames are late, the dim simply shows up at the end. It is never stuck hidden.
    3. Keep exiting as it is. The exit animation never hides the element ahead of time.
    4. Add a unit test for the web Backdrop: it should never render visibility: hidden, and it should end at backdropOpacity.

    I tried this locally. With frames delayed the same way, the backdrop is visible at opacity: 0.72. With normal frames it still fades 0 → 0.72 in about 120 ms using the existing easing. Only the web file changes, so native is untouched.

    What alternative solutions did you explore? (Optional)

    • Drive the backdrop with a shared value and withTiming, like Container/index.web.tsx does. This also works, but it puts a per-frame JS animation where a CSS transition is enough.
    • Drop the backdrop fade-in on web. This is the simplest change, but the 120 ms dim fade is lost.
  14. melvin-bot commented on Oct 16, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  15. mountiny commented on Oct 16, 2026

    @mountiny
    Contributor

    Assigning @nkdengineer as they can reproduce

  16. nkdengineer commented on Oct 19, 2026

    @nkdengineer
    Contributor

    Proposal

    What is the root cause of that problem?

    The web backdrop fades in through a Reanimated Keyframe passed as entering:

    const Entering = useMemo(() => {
    if (!backdropOpacity) {
    return;
    }
    const FadeIn = new Keyframe(getModalInAnimation('fadeIn', backdropOpacity));
    return FadeIn.duration(animationInTiming).reduceMotion(ReduceMotion.Never);
    }, [animationInTiming, backdropOpacity]);

    On web, Reanimated runs an entering keyframe like this:

    1. It renders the element with visibility: hidden.
      https://github.com/software-mansion/react-native-reanimated/blob/d36edbce863e3bfa63bad010247d03c2d330a367/packages/react-native-reanimated/src/createAnimatedComponent/AnimatedComponent.tsx#L408-L419
    2. It doesn't start the animation right away. It waits for the next requestAnimationFrame.
      https://github.com/software-mansion/react-native-reanimated/blob/d36edbce863e3bfa63bad010247d03c2d330a367/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts#L210-L216
    3. It also starts a setTimeout that deletes the generated @keyframes rule. Our web animation lasts only 1 ms (DEFAULT_IN = 1), so the timeout is about 160 ms.
      https://github.com/software-mansion/react-native-reanimated/blob/d36edbce863e3bfa63bad010247d03c2d330a367/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts#L125-L140
    4. It shows the element again only in onanimationstart. The cleanup doesn't restore visibility.
      https://github.com/software-mansion/react-native-reanimated/blob/d36edbce863e3bfa63bad010247d03c2d330a367/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts#L268-L285

    So if the next animation frame comes after the 160 ms cleanup, the animation is applied using a keyframe that no longer exists. It never starts, animationstart never fires, and the backdrop stays visibility: hidden forever.

    This happens when:

    • The tab is in the background. Browsers pause requestAnimationFrame there but still run timers. I can reproduce this reliably: open the app in two tabs and trigger the modal while the tab is not focused. When you switch to that tab, the modal shows without the backdrop. If the tab is focused when the modal opens, the backdrop shows normally.
    • The main thread is busy during startup. This is the case in the report, because this modal opens by itself through GlobalModals, while most modals open after a click when the page is idle.

    The modal card still shows because Container doesn't use an entering keyframe. It animates a shared value instead:

    if (isInitiated.get()) {
    return;
    }
    isInitiated.set(true);
    initProgress.set(
    withTiming(
    1,
    {
    duration: animationInTiming,
    easing,
    // on web the callbacks are not called when animations are disabled with the reduced motion setting on
    // we enable the animations to make sure they are called
    reduceMotion: ReduceMotion.Never,
    },
    onOpenCallBack,
    ),
    );
    }, [animationInTiming, onOpenCallBack, initProgress, isInitiated]);
    // instead of an entering transition since keyframe animations break keyboard on mWeb Chrome (#62799)
    const animatedStyles = useAnimatedStyle(() => getModalInAnimationStyle(animationIn)(initProgress.get()), [initProgress]);

    What changes do you think we should make in order to solve the problem?

    In Backdrop/index.web.tsx, fade the backdrop in the same way Container already fades the card in, and stop using the entering keyframe:

    • Add a shared value progress. In an effect, animate it to 1 with withTiming (same duration, easing and ReduceMotion.Never as before) when isBackdropVisible is true, and reset it to 0 when it's false so the fade runs again if the modal reopens.
    • Replace the static opacity style with useAnimatedStyle: opacity = progress * backdropOpacity.
    • Remove entering={Entering} from both Animated.Views. Keep the Exiting keyframe as it is, because exiting animations are applied right away and don't have this race.

    Without an entering animation, Reanimated never hides the element. A late animation frame can only delay the fade, so the backdrop can't get stuck hidden. This fixes every modal that uses this backdrop, not just the app review modal. Native isn't affected.

    What alternative solutions did you explore? (Optional)

  17. nkdengineer commented on Oct 19, 2026

    @nkdengineer
    Contributor

    @MelvinBot Let review this proposal.

  18. MelvinBot commented on Oct 19, 2026

    @MelvinBot
    ContributorAuthor

    Proposal review

    The root cause is correct and the fix is sound. I recommend accepting it, with two small notes below.

    Root cause verified. I checked each step against Reanimated 4.7.0, the version in App:

    • The entering keyframe is applied only inside requestAnimationFrame (componentUtils.ts:210-216).
    • The @keyframes rule is deleted by a timer. With our 1 ms web duration, the timer is max(5, 160.001) ≈ 160 ms (domUtils.ts:125-140).
    • Only onanimationstart restores visibility. The cleanup callback doesn't touch it for entering animations (componentUtils.ts:268-285).

    So a frame that arrives after the timer leaves the backdrop hidden for good. The background-tab repro fits this well. Background tabs pause requestAnimationFrame but still run timers. Tab restore or opening the app in a background tab is also a realistic way users hit this at startup.

    Fix. Using a shared value plus withTiming matches what Container/index.web.tsx:52-53 already does to avoid entering keyframes. It also fixes every modal that uses this backdrop. Keeping the Exiting keyframe is fine, because exiting animations are applied right away without requestAnimationFrame.

    Notes for the PR:

    1. Keep the animated style in the same spot in the style array, before the caller's style (Backdrop/index.web.tsx:63). That way callers that override opacity still behave the same.
    2. Add the background-tab repro to the PR test steps. Also check that a quick close and reopen still shows the backdrop, because Backdrop stays mounted while isBackdropVisible toggles (ReanimatedModal/index.tsx:229).

    I verified this from the code only. I didn't run the repro in a browser.


    view run

  19. nkdengineer commented on Oct 19, 2026

    @nkdengineer
    Contributor

    @MelvinBot Implement this

  20. MelvinBot commented on Oct 19, 2026

    @MelvinBot
    ContributorAuthor

    🤖 Draft PR created: Fix web modal backdrop staying hidden when it opens during a delayed animation frame
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    The PR follows your proposal. It changes Backdrop/index.web.tsx so the backdrop fades in with a shared value and withTiming instead of the entering keyframe. The Exiting keyframe is unchanged.

    It also adds a unit test that checks the web backdrop renders without an entering animation. The test fails on main and passes with the fix.

    Lint, typecheck, formatting, spelling, the React Compiler check, and the related modal tests all pass. I couldn't check it in the browser because the test session showed a blank page before any modal opened. Please run the background-tab repro and a close-and-reopen check before you mark it ready.


    view run · no recording available

  21. self-assigned this
    on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorReviewingHas a PR in reviewWeeklyKSv2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions