Skip to content

Glass material is lost when a LiquidGlassView is re-presented inside a RN Modal (never reapplied after first mount) #47

Description

@vtquy98

Summary

A LiquidGlassView inside a React Native <Modal> renders its glass material the first time the modal is presented, and renders completely flat on every presentation after that. The same buttons on the screen behind the modal, which never unmount, keep their material for the life of the app.

The root cause looks like the effect only ever being applied once, from layoutSubviews, with nothing that reapplies it when the view is attached to a different window. A UIVisualEffectView binds its backdrop to the window it is in, and RN's modal presents into its own UIWindow.

Environment

  • @callstack/liquid-glass 0.8.0
  • react-native 0.85.3, New Architecture (Fabric)
  • react 19.2.3
  • iOS 26, physical device (isLiquidGlassSupported === true)

Reproduction

const [open, setOpen] = useState(false)

return (
  <>
    <LiquidGlassView colorScheme="dark" interactive style={styles.button}>
      <Pressable onPress={() => setOpen(true)}><Text>open</Text></Pressable>
    </LiquidGlassView>

    {open && (
      <Modal transparent animationType="none" visible onRequestClose={() => setOpen(false)}>
        <View style={styles.fill}>
          <LiquidGlassView colorScheme="dark" interactive style={styles.button}>
            <Pressable onPress={() => setOpen(false)}><Text>close</Text></Pressable>
          </LiquidGlassView>
        </View>
      </Modal>
    )}
  </>
)
  1. Open the modal — the button inside it has a glass material.
  2. Close it.
  3. Open it again — the button inside the modal is flat. The button outside is still fine.

Reopening never recovers. Killing and relaunching the app resets it, so the first presentation works again.

What I found in the source

Everything below is read from the shipped source; the behaviour above is what I observed on device.

1. The effect is applied once and never reapplied

ios/LiquidGlassView.swift:36-43:

public override func layoutSubviews() {
  if (self.effect != nil) { return }
  setupView()
  ...
}

There is no prepareForRecycle in ios/LiquidGlassView.mm, so a recycled view comes back from Fabric's pool with self.effect still set and isFirstMount == false. layoutSubviews then returns immediately. The only other path to setupView() is updateProps, which runs it only when a glass prop actually changed (ios/LiquidGlassView.mm:125) — on a recycle the props are usually identical, so nothing rebuilds the material.

layoutSubviews also never calls super.layoutSubviews(), which I would expect UIVisualEffectView to need for its backdrop and contentView.

There is no didMoveToWindow, so nothing reacts to the view being attached to a different window.

2. Reassigning .effect on the existing view is not enough

I tried forcing setupView() to run on the existing view by toggling a glass prop after mount (interactive, then colorScheme). setupView does run — toggling effect to 'none' visibly broke the first presentation, which proves the path is reached — but the material still does not come back. Only genuinely remounting the component (a changed React key, so a fresh native view is mounted into the presented window) restores it.

3. A view mounted under an alpha-0 ancestor never builds a material at all

If the LiquidGlassView is mounted while an ancestor is at opacity: 0 and only faded in afterwards, the material never appears, even at full opacity later. Because the effect is applied once and layoutSubviews early-returns from then on, there is no second chance. Keeping the ancestor at ~2% opacity instead of 0 is enough to make it build.

4. effect="none" leaves the view unrecoverable

ios/LiquidGlassView.swift:63-67 assigns a bare UIVisualEffect() and carries a TODO about it. Once a view has taken that path, later assigning a real UIGlassEffect does not bring the material back in my testing.

5. animated defaults to applying the effect inside a UIView animation

ios/LiquidGlassView.swift:85-99: after the first mount, applyEffect assigns .effect inside UIView.animate. When that happens while the view is not yet on-window, the assignment appears to be dropped. Passing animated={false} avoids that branch, though on its own it does not fix the modal case.

Suggested fixes

  • Implement prepareForRecycle on the component view and reset the impl (effect = nil, isFirstMount = true) so a pooled view rebuilds.
  • Call super.layoutSubviews().
  • Rebuild the effect in didMoveToWindow when window != nil, so a view that moves between windows rebinds its backdrop.
  • Consider not swallowing the rebuild when effect != nil — or tracking the window the effect was built for.

Current workaround

For anyone hitting this before a fix lands, what worked for us:

  • Bump a counter in Modal's onShow and use it as a React key on the glass views so they are remounted into the presented window.
  • Keep the transition at progress 0 until onShow fires, so that remount is not visible.
  • Never let an ancestor of a glass view sit at exactly opacity: 0 while it mounts — we floor it at 0.02.
  • Pass animated={false} everywhere.

Happy to test a patch, or to open a PR if the maintainers would like the direction above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions