What You Will Build

A gallery of overlapping cards arranged in a fanned stack. Tapping any card brings it to the front with a spring animation while the others fan out behind it. Each card has a different gradient color and SF Symbol. A dot indicator below shows which card is currently focused.

Why This Pattern Matters

Stacked card layouts are popular in photo galleries, dating apps, and product showcases. This component teaches precise ZStack layering with zIndex, offset-based fanning, and scale transforms that create depth perception.

Key SwiftUI Concepts

  • ZStack with zIndex to control which card renders on top.
  • Offset and scaleEffect calculated from the distance to the selected index.
  • Spring animation for natural card movement.

Step 1: Define the Data

@State private var selectedIndex = 0
let symbols = ["star.fill", "heart.fill", "bolt.fill",
               "moon.fill", "sun.max.fill", "cloud.fill"]
let colors: [Color] = [.yellow, .red, .orange, .purple, .cyan, .blue]

Step 2: Build the Card Stack

Each card's position depends on its distance from the selected index. Cards farther away shift right and scale down:

ZStack {
    ForEach(symbols.indices.reversed(), id: \.self) { index in
        let offset = CGFloat(index - selectedIndex)
        RoundedRectangle(cornerRadius: 20)
            .fill(colors[index].gradient)
            .frame(width: 250, height: 320)
            .overlay {
                Image(systemName: symbols[index])
                    .font(.system(size: 60))
                    .foregroundStyle(.white)
            }
            .shadow(color: .black.opacity(0.15), radius: 10, y: 5)
            .offset(x: offset * 15, y: abs(offset) * 10)
            .scaleEffect(1 - abs(offset) * 0.05)
            .zIndex(Double(symbols.count - abs(Int(offset))))
            .onTapGesture {
                withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
                    selectedIndex = index
                }
            }
    }
}
.frame(height: 350)

Step 3: Add Dot Indicators

HStack(spacing: 12) {
    ForEach(symbols.indices, id: \.self) { index in
        Circle()
            .fill(index == selectedIndex ? colors[index] : .gray.opacity(0.3))
            .frame(width: 10, height: 10)
    }
}

How the Offset Math Works

The key insight is using the signed distance from the selected index. If the selected index is 2 and a card is at index 4, the offset is +2. This pushes it 30 points right (2 * 15) and 20 points down (abs(2) * 10). Cards to the left get negative offset, fanning them the other direction.

The scaleEffect shrinks distant cards by 5% per position, and zIndex ensures the focused card always renders on top.

Tips and Pitfalls

  • Reverse the ForEach: Using .reversed() ensures the first card draws on top by default. Without it, the last card would cover everything.
  • Use abs() for symmetric fanning: Cards on both sides of the selection fan out equally.
  • Spring response 0.4 with damping 0.7 gives a snappy but controlled card flip feel.
  • Add swipe gestures: In production, add a DragGesture to swipe between cards for a Tinder-like experience.

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.