What You Will Build

A button that physically expands and flattens when pressed, with the text letter-spacing widening for a satisfying tactile feel. When the finger lifts, the button snaps back to its resting state. This microinteraction is ideal for download, subscribe, and purchase buttons where you want the tap to feel weighty and intentional.

Why This Pattern Matters

Default SwiftUI buttons provide minimal visual feedback on press. iOS users expect responsive touch feedback — Apple's own App Store download button subtly compresses on press. Building a custom press animation with DragGesture(minimumDistance: 0) gives you fine-grained control over the pressed and released states without needing a custom ButtonStyle.

Key SwiftUI Concepts

  • DragGesture(minimumDistance: 0) — captures press and release without requiring actual drag movement.
  • simultaneousGesture — ensures the gesture does not conflict with parent scroll views.
  • .kerning() — animates letter spacing for a stretching text effect.
  • Implicit animation with .animation(.default, value:) for smooth state transitions.

Step 1: Define the Expanding Button

The button uses a Bool state expand to toggle between pressed and resting states. When pressed, the button widens, flattens, and the letter kerning increases:

struct ExpandingButton: View {
    @State var expand = false
    var title: String
    var color: Color
    var action: () -> Void

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: expand ? 5 : 10)
                .frame(width: expand ? 300 : 190, height: expand ? 25 : 50)
                .foregroundStyle(color)
            Text(title)
                .foregroundStyle(.white)
                .fontWeight(.bold)
                .kerning(expand ? 15 : 5)
                .frame(width: 300, height: 50)
                .offset(x: expand ? 8 : 5)
        }
        .animation(.default, value: expand)
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .onChanged { _ in expand = true }
                .onEnded { _ in
                    expand = false
                    action()
                }
        )
    }
}

Step 2: Use Multiple Buttons Together

Stack several buttons with different colors and labels to create a cohesive action panel:

struct ExpandButtonDemo: View {
    var body: some View {
        VStack(spacing: 30) {
            ExpandingButton(title: "DOWNLOAD", color: .blue) {
                // handle download
            }
            ExpandingButton(title: "SUBSCRIBE", color: .green) {
                // handle subscribe
            }
            ExpandingButton(title: "PURCHASE", color: .orange) {
                // handle purchase
            }
        }
    }
}

How It Works

The trick is using DragGesture(minimumDistance: 0) instead of a tap gesture. A tap gesture only fires on release, so you cannot animate the pressed state. A zero-distance drag gesture fires onChanged the instant the finger touches down, letting you set expand = true immediately. On release, onEnded resets the state and fires the action callback.

The .simultaneousGesture modifier is used instead of .gesture so the button works correctly inside ScrollView or List without stealing the scroll gesture.

Tips and Pitfalls

  • Why not ButtonStyle? A custom ButtonStyle with isPressed works too, but gives you less control over the animation timing. The DragGesture approach lets you add delays or multi-stage animations.
  • Kerning offset: When kerning increases, the text shifts slightly. The .offset(x:) compensates for this visual shift.
  • Corner radius animation: The radius shrinks from 10 to 5 when expanded, giving the button a flatter, more "pressed into surface" look.
  • Accessibility: Add .accessibilityAddTraits(.isButton) since this is not a native Button view.
  • Haptic feedback: Add UIImpactFeedbackGenerator(style: .light).impactOccurred() in onChanged for a tactile response.

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.