Updated 14 min readReact NativeExpoiOSLiquid GlassSDK 55

Liquid Glass worked in Expo SDK 54. Then we upgraded to 55.

Six months of stable Liquid Glass on expo-glass-effect, broken by the SDK 55 upgrade. Three weeks of remediation, two parallel glass paths, a pile of workarounds – three retired by the latest Expo patches, two still biting in production, and a wider question about what a cross-platform Glass primitive could look like.

Liquid Glass on iOS worked across our app on Expo SDK 54. The upgrade to SDK 55 broke it on the surfaces where it matters most: first-frame home-screen cards, views that scroll off and back, anything inside a React Native Modal. We shipped remediation over the following three weeks. The architecture we landed on keeps the app shipping but isn't what we'd call finished.

Update · 16 April 2026

We went back to check whether the workarounds below are still needed on the current SDK 55 patch line. Wrote a small test harness that reproduces each failure with its workaround disabled, ran it on simulator, then on a production build on a real device.

Versions bumped for the re-test

  • @expo/ui: 55.0.5 → 55.0.11
  • expo-glass-effect: 55.0.8 → 55.0.10
  • expo-blur: 55.0.10 → 55.0.14
  • expo: 55.0.8 → 55.0.15
  • expo-notifications: 55.0.13 → 55.0.19 (forced; 55.0.13 won't link against the new expo-modules-core, references RCTSharedApplication out of scope)

What's removable

Three of the five testable workarounds cleared both simulator and production device:

  • Host-glass scrolled offscreen and back without visibility polling (item 3)
  • Concurrent Host unmount without the setTimeout stagger (item 4)
  • Parent Pressable around a default-path GlassView without the pointerEvents override (item 6)

We'll drop those in follow-up commits.

What isn't

The Modal touch death (item 5) reproduces cleanly on device; simulator had missed it entirely. The InsideModalContext fallback to BlurView stays. Measured repro lives in the companion Fabric post, since the mechanism is a Fabric touch-routing issue rather than a Liquid Glass one.

The first-frame paint bug (item 1) is the more interesting result. A minimum-repro test, a plain GlassView on a cold-mounted tab, passes on device. So we tried removing the expoui workaround from our real HeroShiftCard on the home screen – same versions, same device, same SDK patches – and the cold-mount transparency came straight back. The only difference between the working path and the broken one, at that point, is whether the glass goes through @expo/ui/swift-ui Host or expo-glass-effect directly, inside that specific component's mount context.

That's the "fixed in the repo, not in the wild" distinction made concrete. The patch clears the isolated case. It doesn't clear the mount context a real app actually ships. SwiftUIGlassView stays for affected surfaces, and we're now more confident than we were that other teams upgrading to SDK 55 are probably hitting the same thing.

The setup

We adopted expo-glass-effect in September 2025, a few days after Expo SDK 54's official release. A single wrapper component (StandardBlurViewReduxView) routed all iOS glass through GlassContainer + GlassView, with BlurView as the Android, iOS <26, & reduce-transparency fallback. From early September 2025 through mid-March 2026 the wrapper ran across the app and we filed no glass-related regressions against ourselves. That's the baseline.

The upgrade

SDK 55 landed in our repo on 20 March 2026. The first glass-adjacent failure – Android BlurView misbehaving – was committed the next day. Over the following week we hit a cluster of distinct iOS Liquid Glass regressions, and the work to deal with them spans three weeks of commits.

Up front: we can't prove every problem below was caused by the SDK 55 upgrade alone. iOS itself updated in the same window, we may have added glass to new surfaces between September and March, and the 54-to-55 diff is nontrivial. What we can say is that the failure modes below were not present in our app on SDK 54, all the remediation was written post-upgrade, and at least two of the specific bugs we describe are now fixed upstream in Expo's own tracker (linked inline). At the time we were writing the remediation we were on @expo/ui 55.0.5 and expo-glass-effect 55.0.8 – i.e. we had already picked up the PR-linked fixes. The failures persisted, which meant either our triggers weren't the ones those PRs addressed, or further patches landed quietly after ours. See the update block at the top of this post for what we've since learned.

1. Glass missing on the first frame of navigator transitions

The most visible regression was on our home screen. On SDK 54, glass cards on the ComingUp tab painted with the effect applied on first render. On SDK 55, they painted transparent and stayed that way for the whole time the tab was mounted – there was no fade-in and no self-recovery. Navigating to another tab and back re-mounted the views and the glass then painted correctly. Every cold open of the affected tab showed broken glass until the user navigated away.

Our own code comments in SwiftUIGlassView.tsx attribute this to a "didMoveToWindow race" – the native UIGlassEffectView not being attached to its window at the moment the effect is applied. That framing turns out to match an issue Expo fixed in March 2026: #43732 ("Liquid Glass disappears after tab change") was closed by PR #43771, whose fix description is "recreates the UIGlassEffect when the view attaches to the window". Same window-attachment mechanism. Related: #43743 (maintainer-acknowledged within hours, merged Mar 9), and #41025 still open for the navigation-transition flicker case.

The workaround was to stop using expo-glass-effect on the affected surfaces and use @expo/ui/swift-ui Host directly, which for whatever reason doesn't have the same first-frame gap. We wrapped that in a new component, SwiftUIGlassView, and exposed it via an opt-in expoui prop on the existing StandardBlurViewReduxView. Views that set expoui go through the Host path. Everything else stays on expo-glass-effect.

Device re-test (16 April): a minimum-repro test, a plain GlassView on a cold-mounted tab, passes on device. But when we removed the expoui workaround from our real HeroShiftCard on the home screen, the cold-mount transparency came straight back. Same build, same device, same SDK patches. The only difference between the working path and the broken one, at that point, is whether the glass goes through @expo/ui/swift-ui Host or expo-glass-effect directly, inside that specific component's mount context. SwiftUIGlassView stays for the affected surfaces.

Worth naming a nuance about the second possibility, because it applies more broadly than just to us. A fix that works in isolation – a bare GlassView on an otherwise empty screen – isn't necessarily a fix in the wild. Teams upgrading to SDK 55 aren't mounting bare GlassViews. They're mounting them inside existing component hierarchies, wrapper conventions, prop architectures, animation orchestration, and state stores that all have their own timing. If any of that interacts with the mount-time window-attachment in a way the patch doesn't account for, the bug stays live in that team's reality regardless of whether it's closed in the issue tracker. Fixed in the repo and fixed in the wild aren't the same thing.

2. wrap and background modes, and why they both exist

Once we were routing first-frame-sensitive surfaces through the Host, a second problem appeared. Native RNHostView bridges its React Native subtree into SwiftUI's layout system. For static cards this works. For anything with lifecycle (size changes, re-renders, mount/unmount cycles, animations) we saw layout failures, frozen content, and outright crashes.

We can't fully isolate which of those specifically triggers the underlying SwiftUI layout-solver issue. Our code comments only document the crash-prone cases explicitly ("ScrollViews, pagers, and complex layouts that crash inside RNHostView"). The broader failure set is field experience. Adjacent issues in Expo's tracker – #41313 (SwiftUI Button crash on iOS 26), #40911 (UIGlassEffect selector not found), #40354 (SwiftUI TextField inside Section crash), #43866 (@expo/ui version coupling to an unreleased expo-modules-core) – all suggest the Host / RNHostView surface is new and unstable enough that the "complex children break things" phenomenon is broader than a single bug. The rule we landed on: if the child has any lifecycle behaviour at all, assume the naïve wrap won't work.

SwiftUIGlassView therefore has two modes, selected per call site:

  • wrap: children render inside the Host's RNHostView. Works for static cards. Breaks for anything with motion or re-rendering. Clean first-frame paint.
  • background: Host contains a sized dummy View (a plain RN View with explicit width/height – Spacer doesn't trigger glass rendering, per our own code comment). The real React Native tree lives outside the Host as a sibling, with pointerEvents="none" on the glass layer so touches reach the content. Robust to any child, with a one-pass delay: the glass can't mount until onLayout has measured the content, so the first render has no glass and the second render does.

Neither is the correct default. We pass expoui="wrap" when the child is static and first-frame paint matters, expoui="background" when the child has any lifecycle. About twenty components in the app currently pass expoui: home-screen cards, Job / Daylight / Weather / LivePay cards, the import-screen toolbars. Everything else stays on the default expo-glass-effect path.

3. The Host hangs the app when scrolled off

Separate failure mode: the Host gets into a state where the app becomes unresponsive if the component has been scrolled offscreen. Scroll a glass card out of view on a long list, scroll back, the JS thread has stalled and touches no longer respond. Force-quit and relaunch is the only way out.

Our workaround is per-component visibility polling. SwiftUIGlassView runs a 200ms measureInWindow interval while mounted, computes whether the view's bounds overlap the screen (with a 200px margin for early mount and late unmount), and toggles local state to mount or unmount the Host accordingly. The glass layer disappears when the component is clipped and reappears when it scrolls back into view.

This is a cost we didn't carry on SDK 54. It's a setInterval per glass instance. We haven't profiled the real-world impact carefully, but on a long list with many glass cards it's non-zero. Want to remove it once there's upstream movement.

4. Multiple Hosts unmounting in the same frame hang the app

A worse-flavoured cousin of (3). If several Hosts unmount on the same frame (for example, hiding a batch of cards when exiting an Edit mode) the JS thread can hang. Same symptom as the scroll-off case: unresponsive app, force-quit required.

We haven't attributed this to a specific root cause ourselves. The closest public match is #43537 ("Expo UI menu with glass button + native view freezes app on iOS"), which was fixed by PR #43570. The PR description says it "converts RNHostView to a SwiftUI view and resolved the freezing issue as a side effect" – we suspect the same underlying mechanism bites us during multi-Host teardown, though we haven't confirmed it. The workaround is to stagger unmounts. In SwiftUIGlassView's cleanup effect we increment a module-scoped counter and schedule a no-op setTimeout(..., slot * 50) so each Host's teardown gets its own 50ms window:

let unmountSlot = 0;

useEffect(() => {
  return () => {
    const slot = unmountSlot++;
    setTimeout(() => {}, slot * 50);
  };
}, []);

Not a fix. Keeps the app alive.

5. Host inside an RN <Modal> swallows touches

Putting a Host-based glass view inside React Native's <Modal> causes Pressable children to fire onPressIn but not onPress. Taps produce the press-in visual and then nothing – the press terminates mid-sequence.

The mechanism we document in our own InsideModalContext.tsx header is that UIGlassEffectView and RNHostView both install native UIKit gesture recognizers, and RN's <Modal> on iOS presents a separate UIWindow. The native recognizers inside the Modal's window compete with RN's touch system and win inconsistently. That explanation lines up with the symptom but we haven't proven it.

The avoidance is mechanical. InsideModalContext is a React context set to true by our modal wrappers (StandardModal, StandardWideModal, and the Portal-based custom sheet from the previous post). Both StandardBlurViewReduxView and SwiftUIGlassView read the context, and if it's true they short-circuit to the plain BlurView fallback. BlurView doesn't install gesture recognizers, so touches route normally.

6. The default expo-glass-effect path has its own gesture issue

Everything above is remediation on the @expo/ui/swift-ui opt-in path. The default expo-glass-effect path, which most of the app still uses, has a separate gesture issue. UIGlassEffect installs its own native gesture recognizer that can intercept taps destined for a parent Pressable.

Our fix is in StandardBlurViewReduxView.tsx: when the caller passes hasPressHandler, we set pointerEvents="none" on both GlassContainer and GlassView. The native recognizer never sees the touch, so the parent Pressable gets it clean.

This one isn't cleanly SDK-55-boundary in the same way – we can't point at a clean regression point in the commit trail – but we didn't need it on SDK 54. Best guess: something about how expo-glass-effect's recognizer registers changed during the 55 release window.

What the architecture looks like now

For iOS glass, StandardBlurViewReduxView routes three ways:

  • expoui prop set and not inside a Modal → SwiftUIGlassView via @expo/ui/swift-ui Host
  • no expoui prop and not inside a Modal → expo-glass-effect's GlassContainer + GlassView
  • inside a Modal → plain BlurView fallback

Call sites pick between expoui="wrap" and expoui="background" based on whether the child has lifecycle.

Workarounds currently carried:

  • hasPressHandlerpointerEvents="none" on the default expo-glass-effect layer, so a parent Pressable gets clean touches.
  • 200ms measureInWindow visibility polling per SwiftUIGlassView instance to unmount before scroll-off.
  • Staggered unmount via setTimeout(slot * 50) on SwiftUIGlassView teardown.
  • InsideModalContext forcing the BlurView fallback inside RN Modals.
  • background mode's dummy-View-plus-sibling-RN-tree construction, and the accompanying one-pass glass delay.
  • patches/expo-blur+55.0.10.patch on Android. Separate saga, not covered here.

None of these feel like the final answer. They keep the app shipping.

What's already filed, what we should file

Items 1, 2, and 4 have public Expo issues with fixes shipped (linked inline in those sections). Items 3, 5, and 6 we hadn't seen publicly reported when we wrote this – they showed up in our app, we built workarounds, and the re-test (see update at the top) found all three clear on the current patch line without the workaround. That could mean Expo resolved them quietly in later patches, or they were an interaction between several bugs that only appeared in our specific wrapper chain. We never isolated them well enough to file.

The one live item is the first-frame cold-mount paint (item 1). The public fix doesn't resolve it for us in production. If you've hit something similar on an SDK 55 upgrade and have a cleaner repro than we do, or a theory about why the published fix wouldn't cover a cold-mount path, we'd like to compare notes: hello@greatworkeveryone.com.

A note on cross-platform native primitives

We want to flag the wider shape of this problem, partly as musing, partly because we don't know if the take is fair. Skip the section if you're here for specific bugs.

Liquid Glass in our app goes through three stacked wrapper components:

  • StandardBlurViewRedux (167 lines). Outer shell. Pressable wrapping with onPressOut and a long-press guard, haptics, disabled state, layout.
  • StandardBlurViewReduxView (211 lines). Routing layer. Picks between expo-glass-effect, @expo/ui/swift-ui Host when expoui is set, BlurView when reduce-transparency is on or we're inside a Modal, or a themed translucent View on Android.
  • SwiftUIGlassView (223 lines). The Host integration. Two modes, visibility polling, staggered unmount, dummy-View-for-paint in background mode, modal-context bailout, lazy SwiftUI require.

None of that is gratuitous. Each piece exists because a specific failure mode bit us on a real device, or because a cross-platform fallback needed to exist that the library doesn't provide. But it's about 500 lines of plumbing for a frosted card on a home screen. A new RN team hitting the same problem in 2026 has to either re-derive each failure mode or find a writeup like this one.

Worth saying out loud: should a cross-platform mobile framework ship something like this as a first-class primitive? Liquid Glass today is iOS 26+ only, with a plain View fallback everywhere else. No BlurView fallback. No Android equivalent. The cross-platform behaviour is entirely something you compose yourself by wrapping the iOS component in enough conditional logic to make it look reasonable on platforms where it doesn't exist. Every app shipping Liquid Glass has to build, or copy, roughly what we built.

This isn't an accusation. Expo has shipped an extraordinary amount over the last few years: expo-router, EAS, the dev client, native tabs, the Fabric migration, and Liquid Glass support itself. The observation is more about direction. A cross-platform Glass primitive with honest fallbacks – BlurView on iOS <26, the cleanest available material on Android, a themed translucent view when nothing else applies, and the gesture / touch / first-frame / scroll-off behaviours all centralised – would save every downstream team from rebuilding the same 500 lines and hitting the same device-only bugs on the way.

Caveat: we've never shipped anything substantial in Flutter or Kotlin Multiplatform or SwiftUI's own cross-platform story, so we don't know how those frameworks handle the "new iOS-only UI primitive" problem. If someone working in one of them reads this, we'd like to know what the equivalent of this post looks like on your side. Maybe everyone pays this tax. Maybe there's a better way. We'd just like to compare.

Written while shipping Shift It 3.0. Available April 20, 2026.