Work/Liquid Glass
Liquid Glass
A React Native component library. Apple's native glass on iOS 26, and a pixel-faithful JS and SVG fallback everywhere else. One component, the right tier per device.
- ROLE
- Design & development
- YEAR
- 2025–2026
- PLATFORM
- Private library
- STATUS
- In production

ParticleRings, fifteen hundred orbs, running
05 / 05
Screen 5 of 5: ParticleRings, fifteen hundred orbs, running
One component should not make the app author think about which version of iOS the user happens to be running.
On an iPhone running iOS 26, a glass button is Apple's own material, and it tracks your finger the way the operating system does. On a phone from 2018 it is a reconstruction in JavaScript and SVG that almost nobody notices is a reconstruction. The line the app author writes is identical, and keeping it identical is the entire product.
Six of the eighteen packages are Swift, and none of them are the glass. They are the parts that cannot be faked at all: the scan camera, subject lift, text recognition, Apple Health, home-screen widgets. The glass has a fallback because a fallback is possible. A camera does not.
It is not a demo. It runs in production inside TeamTipOut and two other apps, which is the only reason I know what is actually wrong with it. Every one of those apps found something a preview never would.
The whole library is one bet: that the tier decision belongs inside the component, not in the app. Everything else follows from that.
In production inside TeamTipOut and two other apps, and not published. It is the library client work is built from, so a project that uses it starts on components that have already been through a store review rather than on a blank file. Design and development entirely my own.
- STATUS
- In production
- RELEASE
- Private: used on my own builds
- SHIPPING SINCE
- 2025
59 components.Eighteen packages, six of them native.
There are no screens to show you, because a library does not have any. What it has is parts. These, in the gallery app they are all built against.
- PACKAGES
- 18
- COMPONENTS
- 59
- NATIVE MODULES
- 6
- APPS IN PRODUCTION
- 3
Foundations
Text · Button · Input · OtpInput · InlineErrorNotice · SectionHeader
Indicators
AnimatedCheckIndicator · AnimatedRadioIndicator · CheckmarkGlyph · ProgressBar · MetricBar · MetricProgressBar · ScoreRing · SmallProgressRing · SkeletonBlock · AnimatedNumber
Layout & surfaces
SurfaceCard · NavRow · SeeAllSection · ScreenHeader · EdgeFade · Entrance
Sheets & gestures
BottomSheet · CollapsingTitle · FloatingSheet · SheetSubmitCta · CloseButton · SwipeableActionRow
Liquid glass
LiquidGlassSurface · LiquidGlassView · LiquidGlassButton · LiquidGlassIconButton · LiquidGlassSwitch · LiquidGlassSlider · LiquidGlassSegmentedControl · LiquidGlassTabBar · LiquidGlassMenu
Skia motion
ParticleRings · AmbientDust · CelebrationRing · DotWaveField · ShaderLoadingTrack
Optional-peer subpaths
LargeScoreRing · RemoteImage
Charts
BarChart · RadarChart
Habit streak
StreakCalendar · StatusRingDayCell · MilestoneBadge · MilestoneTrackSegment · ActivityLegend
Monetization
PremiumBlur · PremiumSectionLock · PremiumScreenLock
Providers & scroll effects
ThemeProvider · FontProvider · ScrollAwareTopBlur · SkiaReframeImage
Wizard
WizardLayout
Two decisions worth explaining.The parts that took the longest.
D—01
Nobody asks what iOS this is
The obvious approach is two components and a version check in the app. That pushes the problem onto every author who uses the library, and guarantees the two paths drift apart the moment one of them gets a fix.
So the library never asks what iOS it is running on. It asks whether Apple's glass module loaded, and then whether that module says its own API is available. A package can be installed on a phone too old to run it, and the version number would have told you the wrong thing either way. Two questions, no version numbers, and the second one is Apple's answer rather than mine.
The cache has three states, not two. Undefined means nobody has asked yet; null means the question has been asked and answered no. Without that distinction a device without the module retries a failing require on every single render of every glass surface on screen.
let resolved: GlassEffectModule | null | undefined; if (resolved !== undefined) return resolved;resolved = null;if (Platform.OS !== 'ios') return resolved; try { const mod = require('expo-glass-effect'); // installed is not the same as supported if (mod.isGlassEffectAPIAvailable?.() && mod.GlassView) resolved = mod;} catch { resolved = null; }D—02
What production taught me
A New Architecture crash on a fractional slider value. A segmented control that broke when it was measured before its labels had finished laying out. Neither appears in a preview app, because a preview app never holds a value from a real user or renders under real layout pressure.
Both were found by using the library inside TeamTipOut. That is the argument for shipping your own tools into your own product. The bugs that matter are the ones that only surface when something actually depends on it.
## 0.9.2, found in TeamTipOutfix: clamp slider value before the bridge 0.5 crossed as double, Swift wanted a step ## 0.9.1, found in TeamTipOutfix: defer segmented measure until layout width read before the font had loadedD—03
Fifteen hundred orbs, one draw call
The recording at the top of this page is the library's heaviest view, and it is there because a still cannot show you the only thing worth showing: that the frame rate holds.
Fifteen hundred soft orbs is a number chosen to be uncomfortable. Drawn the obvious way it is fifteen hundred draw calls a frame and a phone that gets warm. Drawn as one batched atlas it is a single GPU call, and the per-frame work becomes proportional to the number of orbs rather than to the number of pixels they cover, which is why the swarm can be dense without being expensive.
The positions are computed in a worklet on the UI thread, so a busy JavaScript thread cannot stutter the animation. Every tuning constant sits in plain JavaScript at the top of the file, because a swarm is tuned by looking at it and recompiling to change a radius is how you stop looking.
const DEFAULT_COUNT = 1500; // the tuned densityconst ORB_RADIUS_FACTOR = 0.022; // one batched GPU call, not one per orbSkia.drawAtlas(sprite, transforms, colors); // O(orbs), not O(pixels x orbs)useDerivedValue(() => positionsFor(clock.value));WHAT IT IS BUILT ON
- LANGUAGE
- TypeScript strict · Swift · Objective-C bridging
- RUNTIME
- React Native New Architecture · Fabric · TurboModules
- PACKAGES
- 18 packages · 6 optional native peers
- RELEASE
- Versioned with Changesets · CI on GitHub Actions
CREDITS
Design and development entirely my own.
STATUS
A private library, running in production inside TeamTipOut and two other apps. There is no public repository to link to.
NEXT PROJECT
CoffeeBrew
A coffee machine in an app: Bluetooth pairing, Wi-Fi handover, scheduled brews and a home-screen widget. Written against a protocol rather than a product.
Tell me what you’re building.A written scope and a fixed quote before any money moves.
I reply to everything within one working day. If I’m not the right person for it, I’ll tell you straight away rather than three weeks in.




