Skip to content

Hero Animations (SwiftUI)

A SwiftUI guide for coding agents. Also covers matchedgeometryeffect, shared element transition, zoom navigation transition, matchedtransitionsource, morph view between positions, namespace animation.

A hero animation morphs one view into another through a seamless move, the way a photo grows from a grid cell into a detail screen. matchedGeometryEffect handles it inside one hierarchy; matchedTransitionSource plus navigationTransition handle it across a NavigationStack push.

Matched Geometry Inside One View

Attach matchedGeometryEffect(id:in:) with the same id and @Namespace to the view in both places. When one insertion and one removal share an id in the same transaction, SwiftUI interpolates the frames in window space so a single view appears to travel and resize.

@Namespace private var imageEffect

// grid of small images
Image(systemName: image)
  .resizable()
  .matchedGeometryEffect(id: image, in: imageEffect)
  .frame(width: 44, height: 44)
  .onTapGesture {
    withAnimation {
      allImages.removeAll { $0 == image }
      selectedImages.append(image)
    }
  }

// grid of selected images, larger
Image(systemName: image)
  .resizable()
  .matchedGeometryEffect(id: image, in: imageEffect)
  .frame(width: 88, height: 88)

The mutation must run inside withAnimation; without it the views just swap with the default fade transition. Every matched view needs a unique id within the namespace.

Across a Navigation Push

matchedGeometryEffect does not work between views in a navigation stack. Mark the source with matchedTransitionSource(id:in:) and give the destination a navigationTransition, such as .zoom. iOS 18+.

@Namespace private var namespace

NavigationStack {
  NavigationLink {
    Image(systemName: "globe")
      .resizable()
      .scaledToFit()
      .navigationTransition(.zoom(sourceID: "world", in: namespace))
  } label: {
    Image(systemName: "globe")
      .matchedTransitionSource(id: "world", in: namespace)
  }
}

The source id and the transition's sourceID must match, in the same namespace, or the push falls back to the standard slide.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-hero-animations" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems