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.height for 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 CMMotionManager to tilt the card based on device orientation for a hands-free parallax effect.

iOS Version: iOS 15+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.