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>
)}
</>
)
- Open the modal — the button inside it has a glass material.
- Close it.
- 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.
Summary
A
LiquidGlassViewinside 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. AUIVisualEffectViewbinds its backdrop to the window it is in, and RN's modal presents into its ownUIWindow.Environment
@callstack/liquid-glass0.8.0react-native0.85.3, New Architecture (Fabric)react19.2.3isLiquidGlassSupported === true)Reproduction
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:There is no
prepareForRecycleinios/LiquidGlassView.mm, so a recycled view comes back from Fabric's pool withself.effectstill set andisFirstMount == false.layoutSubviewsthen returns immediately. The only other path tosetupView()isupdateProps, 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.layoutSubviewsalso never callssuper.layoutSubviews(), which I would expectUIVisualEffectViewto need for its backdrop andcontentView.There is no
didMoveToWindow, so nothing reacts to the view being attached to a different window.2. Reassigning
.effecton the existing view is not enoughI tried forcing
setupView()to run on the existing view by toggling a glass prop after mount (interactive, thencolorScheme).setupViewdoes run — togglingeffectto'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 Reactkey, 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
LiquidGlassViewis mounted while an ancestor is atopacity: 0and only faded in afterwards, the material never appears, even at full opacity later. Because the effect is applied once andlayoutSubviewsearly-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 unrecoverableios/LiquidGlassView.swift:63-67assigns a bareUIVisualEffect()and carries a TODO about it. Once a view has taken that path, later assigning a realUIGlassEffectdoes not bring the material back in my testing.5.
animateddefaults to applying the effect inside a UIView animationios/LiquidGlassView.swift:85-99: after the first mount,applyEffectassigns.effectinsideUIView.animate. When that happens while the view is not yet on-window, the assignment appears to be dropped. Passinganimated={false}avoids that branch, though on its own it does not fix the modal case.Suggested fixes
prepareForRecycleon the component view and reset the impl (effect = nil,isFirstMount = true) so a pooled view rebuilds.super.layoutSubviews().didMoveToWindowwhenwindow != nil, so a view that moves between windows rebinds its backdrop.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:
Modal'sonShowand use it as a Reactkeyon the glass views so they are remounted into the presented window.onShowfires, so that remount is not visible.opacity: 0while it mounts — we floor it at0.02.animated={false}everywhere.Happy to test a patch, or to open a PR if the maintainers would like the direction above.