What You Will Build
A gradient card that tilts in 3D as you drag your finger across it. Inner elements (icon, title, subtitle) shift at different speeds to create a parallax depth effect. When you release, the card springs back to its neutral position. This is the effect you see on Apple gift cards and many premium app interfaces.
Why This Pattern Matters
3D parallax effects add perceived depth and premium feel to flat interfaces. Apple uses this on the tvOS focus engine and CarPlay interfaces. The technique combines rotation3DEffect with layered offsets and is surprisingly simple in SwiftUI.
Key SwiftUI Concepts
- rotation3DEffect with separate X and Y axis rotations for realistic tilt.
- DragGesture with onChanged and onEnded for interactive control.
- Layered parallax offsets where each element moves at a different rate relative to the drag.
Step 1: Track the Drag
@State private var dragOffset: CGSize = .zero
Step 2: Build the Parallax Layers
Each text and icon layer uses a different multiplier on the drag offset. The icon moves most (0.15x), the title moves less (0.1x), and the subtitle barely moves (0.05x). This creates the illusion that elements are at different depths:
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(colors: [.purple, .blue, .cyan],
startPoint: .topLeading, endPoint: .bottomTrailing)
)
.frame(width: 280, height: 380)
.shadow(color: .purple.opacity(0.3), radius: 20, x: 0, y: 10)
VStack(spacing: 16) {
Image(systemName: "globe.americas.fill")
.font(.system(size: 60))
.foregroundStyle(.white)
.offset(x: dragOffset.width * 0.15,
y: dragOffset.height * 0.15)
Text("Parallax")
.font(.largeTitle.bold())
.foregroundStyle(.white)
.offset(x: dragOffset.width * 0.1,
y: dragOffset.height * 0.1)
Text("3D Card Effect")
.font(.subheadline)
.foregroundStyle(.white.opacity(0.8))
.offset(x: dragOffset.width * 0.05,
y: dragOffset.height * 0.05)
}
}
Step 3: Apply 3D Rotation
Two separate rotation3DEffect calls handle horizontal and vertical tilt independently. Dividing the drag distance by 10 keeps the rotation subtle:
.rotation3DEffect(
.degrees(Double(dragOffset.width) / 10),
axis: (x: 0, y: 1, z: 0)
)
.rotation3DEffect(
.degrees(Double(-dragOffset.height) / 10),
axis: (x: 1, y: 0, z: 0)
)
Step 4: Add the Drag Gesture with Spring Reset
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation
}
.onEnded { _ in
withAnimation(.spring(response: 0.5, dampingFraction: 0.6)) {
dragOffset = .zero
}
}
)
Tips and Pitfalls
- Negative height rotation: Notice the minus sign in
-dragOffset.heightfor the X-axis rotation. Without it, the card tilts opposite to your finger direction, which feels unnatural. - Keep rotation subtle: Dividing by 10 caps the tilt at about 30 degrees for a full-width drag. Dividing by 5 doubles the effect for a more dramatic look.
- Low damping fraction (0.6) creates a bouncy snap-back. Use 0.8 for a more controlled return.
- Performance: rotation3DEffect is GPU-accelerated. This runs at 120fps on ProMotion displays without optimization.
- Gyroscope alternative: Replace DragGesture with
CMMotionManagerto tilt the card based on device orientation for a hands-free parallax effect.
iOS Version: iOS 15+