← BlogDesignEngineering

Morph, don't pop

Watch how iOS handles a search bar becoming a cancel button, or a compact player expanding into the full-screen one. Nothing disappears and something else appears in its place. One shape stretches, slides, and settles into the next. That's the whole trick, and most apps get it wrong by defaulting to a cross-fade.

1. Popping breaks the illusion of one object

A cross-fade tells the eye "these are two different things." A morph tells the eye "this is the same thing, changing." The second reading is what makes an interface feel alive instead of assembled from screens. iOS's own system controls almost never opacity-swap: the back chevron becomes the close X, the tab bar's selection pill slides instead of jumping to the next tab.

2. The trap: opacity and scale don't do what you'd expect on real glass

If you're using the actual system material (UIGlassEffect, Liquid Glass on iOS 26, not a blurred view standing in for it), animating its opacity to zero doesn't fade it out. It renders blank. Animate its scale to zero and you get a pixelated flash right before it vanishes, because the glass effect is computed at a resolution tied to its live size, not swept smoothly like an ordinary layer.

The fix is mechanical once you know it: never hide or reveal a glass element with opacity or scale. Move it off-screen with translateX / translateY instead, and let the container clip it. The element stays fully rendered the whole time, it just isn't visible, which is exactly the state you want when it re-enters.

3. Build the morph out of one shared shape

A tab bar makes this concrete. The naive version renders each tab's background as a separate view that fades in on selection. Instead, render one glass capsule and translate that single view between tab positions with a spring. Selecting a tab never creates or destroys anything, it just moves the highlight.

Same principle for an action button that needs to become a toolbar: one glass shape, animated width and position, not two shapes cross-fading past each other.

4. Once the primitive exists, it's a spring config, not a rewrite

A back button morphing into a close button and a segmented control's thumb sliding between options are the same handful of lines: one view, translate-only, spring-driven, just different targets. The cost is in building that primitive once and refusing to reach for opacity as a shortcut afterward.

Where to start

Pick the one transition in your app that currently pops (a modal's primary action button is the easiest target) and replace the cross-fade with a single view translating between its two states. If it's backed by native glass, check that view's opacity and scale never leave 1. Everything else about the trap is a search for a stray withTiming(opacity, …) you forgot to delete.


We build exactly these transitions natively, with the glass-safety rules already baked in. See the components →