iMessage Reply Bubble Physics in SwiftUI: The Real Spring
The motion communicates causality, not springiness. Reproduce the legibility and the bubble feels alive.
TL;DR
The iMessage bubble feels alive because motion communicates causality: a sent bubble springs up from the input with overshoot, a received one settles with weight. The clone reproduces that with a restrained spring (.spring(response: 0.35, dampingFraction: 0.7)) on scale and offset, never the frame, since animating width reflows the text and is the classic tell, with sent bubbles springier than received on purpose. The tail is a drawn shape on the last bubble of a speaker run only, the stack springs to make room, and the whole thing stays under 400ms, Reduce-Motion-aware, and never blocks readability. A free VP0 design supplies the chat screen to generate the transitions onto.
What makes the iMessage bubble feel alive?
Not bounce for its own sake. The thing people recognize in iMessage (a service Apple once clocked at 28,000 messages per second) is that bubbles behave like physical objects: a sent bubble springs up from the input field with a little overshoot, an incoming bubble settles in with weight, and the tail anchors it to the right speaker. The motion communicates causality, your message launched, theirs arrived, and that legibility, not the springiness, is what a good clone reproduces.
Getting it wrong reads instantly as off-brand: a linear fade-in feels dead, an over-bouncy spring feels like a toy, and a bubble that animates its width while text reflows looks broken. The craft is a restrained spring on the right properties.
Which spring, and on what?
SwiftUI’s modern spring API does the heavy lifting. The honest distinction is between the two spring families:
| API | You specify | Best for |
|---|---|---|
| .spring(response:dampingFraction:) | Perceptual duration + bounciness | Almost everything; the default |
| .interpolatingSpring(stiffness:damping:) | Physical constants, velocity-preserving | Gesture handoffs where velocity carries |
For the send animation, .spring(response: 0.35, dampingFraction: 0.7) gives the recognizable launch: quick, a touch of overshoot, settled fast. The properties to animate are scale and offset, never the frame: a new bubble enters at scale: 0.5 and an upward offset, springing to scale: 1 at rest, while opacity rides a faster, plainer curve so the bubble is visible before it finishes settling. Animating the bubble’s actual width or height instead makes the text inside reflow mid-animation, which is the single most common tell of a hand-rolled clone.
.transition(.asymmetric(
insertion: .modifier(
active: BubbleEnter(scale: 0.5, y: 20, opacity: 0),
identity: BubbleEnter(scale: 1, y: 0, opacity: 1)
).animation(.spring(response: 0.35, dampingFraction: 0.7)),
removal: .opacity
))
Sent and received differ on purpose: the sent bubble springs more eagerly (lower damping, it is your action), the received bubble settles with slightly more weight (higher damping, it arrived from elsewhere). That asymmetry is subtle and is most of what separates “feels like iMessage” from “has a spring on it.”
How do the tail and the stack behave?
The tail is a shape, not an image, drawn with a Path or an SF Symbol-style bezier merged into the bubble’s rounded rectangle, flipped by sender. It matters because the tail only appears on the last bubble in a run from one speaker; consecutive messages from the same person are tail-less and tucked closer, and rendering a tail on every bubble is an immediate authenticity miss.
The stack is where physics meets layout. When a new bubble springs in, the existing bubbles should make room with their own quick spring rather than teleporting upward, which a LazyVStack inside a bottom-anchored scroll view gives you when insertions are animated and the scroll pins to the newest message. The whole transcript inherits the continuity discipline from the broader motion language, the same family as the bottom-sheet spring work on the cross-platform side: spring the things that should feel connected, cut the things that should feel instant.
What completes the illusion?
Three details, each cheap, each load-bearing. The send-from-input origin: the bubble should appear to leave the text field, which means matching the entry offset to the input’s position rather than springing from nowhere, a small shared-geometry move. Tap and long-press response: a bubble dips slightly on touch-down (scale 0.97) and springs back, the same gesture-aware feedback the platform uses everywhere, wired through SwiftUI gestures, the same touch-down feedback discipline as the Duolingo progress ring animation. And honest reactions: a tapback pops in on its own small spring centered on the bubble’s corner, never blocking the text.
Respect the platform contract that governs all of it: keep durations short (the whole send animation is under 400 milliseconds), honor Reduce Motion by swapping the spring for a plain fade (the bubble still appears, just without the travel), and never let the animation delay the message actually being readable. Apple’s motion guidance is the reference: motion clarifies, it does not perform.
For the surrounding chat screen, the transcript, composer, reaction layer, a free VP0 design supplies the structure, so an agent generates the spring transitions onto a bubble layout that already has the tail logic and run-grouping right.
Key takeaways: the iMessage bubble in SwiftUI
- Animate scale and offset, never the frame: width animation reflows text and is the classic tell.
- One restrained spring (response ~0.35, damping ~0.7), with sent springier than received on purpose.
- The tail is a drawn shape on the last bubble of a run, not on every bubble.
- The stack springs to make room; the scroll pins to the newest message.
- Short, Reduce-Motion-aware, never blocking readability: motion clarifies causality, it does not perform.
Frequently asked questions
How do I build iMessage-style reply bubble physics in SwiftUI? Animate bubble entry with a restrained spring on scale and offset (not the frame), use .spring(response: 0.35, dampingFraction: 0.7) with sent bubbles springier than received, draw the tail as a shape only on the last bubble of a speaker run, and let the stack spring to make room. A free VP0 design supplies the chat screen structure to generate onto.
Why does my chat bubble animation look wrong? Most likely you are animating the bubble’s width or height, which reflows the text mid-flight. Animate scale and offset instead and keep the layout size fixed, so the bubble grows into place without the text rearranging inside it.
Which SwiftUI spring should I use for chat bubbles? .spring(response:dampingFraction:) for the entry animation, since it is specified in perceptual terms and covers almost every case. Reserve .interpolatingSpring for gesture handoffs where an in-progress velocity needs to carry into the animation.
How do iMessage bubble tails work? The tail is a drawn shape merged into the bubble’s rounded rectangle and flipped by sender, and it appears only on the last message in a consecutive run from one speaker. Tail-less, tucked-closer bubbles for the rest of the run are what make a thread read as authentic.
Do I need to support Reduce Motion for bubble animations? Yes: when Reduce Motion is on, swap the spring travel for a plain fade so the bubble still appears without the movement. The message must always become readable immediately regardless of the animation setting.
What VP0 builders also ask
How do I build iMessage-style reply bubble physics in SwiftUI?
Animate bubble entry with a restrained spring on scale and offset rather than the frame, use .spring(response: 0.35, dampingFraction: 0.7) with sent bubbles springier than received, draw the tail as a shape on the last bubble of a speaker run, and let the stack spring to make room while the scroll pins to the newest message. A free VP0 design supplies the chat screen structure.
Why does my chat bubble animation look wrong?
Most likely you are animating the bubble's width or height, which makes the text reflow mid-animation. Animate scale and offset instead and keep the layout size fixed, so the bubble grows into place without the text rearranging inside it.
Which SwiftUI spring should I use for chat bubbles?
Use .spring(response:dampingFraction:) for entry, since it is specified in perceptual duration and bounciness and covers almost every case. Reserve .interpolatingSpring for gesture handoffs where an in-progress velocity must carry into the animation.
How do iMessage bubble tails work?
The tail is a drawn shape merged into the bubble's rounded rectangle and flipped by sender, appearing only on the last message of a consecutive run from one speaker. The earlier bubbles in the run are tail-less and tucked closer, which is what makes the thread read as authentic.
Do chat bubble animations need Reduce Motion support?
Yes: when Reduce Motion is enabled, replace the spring travel with a plain fade so the bubble still appears without movement. The message must become readable immediately in every case, so animation never gates the content.
Part of the Native Apple & SwiftUI: The iOS Ecosystem hub. Browse all VP0 topics →
Keep reading
Loyalty Punch-Card Stamp Animation in SwiftUI
The stamp is the product: a spring with overshoot, a resting tilt, and a haptic thunk, on a count the server owns and the merchant grants.
Interactive Solar System 3D Viewer in SwiftUI
RealityView entities, orbits as pivot rotations, NASA textures, and the scale cheat you must pick on purpose: the 3D solar system that stays smooth.
iOS Context Menu Long-Press Blur in SwiftUI
The native .contextMenu gives the lift, blur, and haptic for free. Build custom only when it is not a menu, and drive all three layers from one spring.
Build a Stock Market Heat Map Grid UI in SwiftUI
A market heat map colors and sizes tiles by gain and market cap. Here is how to build the stock market heat map grid in SwiftUI, with an accessible color scale.
Build a Booking.com-Style Availability Calendar in SwiftUI
A Booking.com-style availability picker is more than a date picker. Here is how to build the availability calendar in SwiftUI, with real open and booked dates.
Build a Sideloading iOS App Install Animation in SwiftUI
In the EU, an alt-marketplace install is a real, system-gated flow. Here is how to build the sideloading install animation in SwiftUI, honestly.