DEV Community

Sebastien Lato
Sebastien Lato

Posted on • Edited on

How to Build a Clean Collapsible Header in SwiftUI

One of my favorite UI patterns in modern iOS apps is the collapsible header — the kind that smoothly shrinks, fades, and moves as you scroll. It makes an app feel dynamic without being distracting.

After implementing multiple versions across my projects, here’s a clean and reusable way to build it in SwiftUI.


🎯 What We'll Build

A header that:

  • Stays large at the top
  • Shrinks smoothly as you scroll
  • Moves upward and slightly fades
  • Expands back when you scroll down

No hacks, no UIKit bridging — just pure SwiftUI.


🧠 Key Idea

We track the scroll offset using a GeometryReader and use that value to compute:

  • height compression
  • opacity
  • vertical offset
  • optional scaling

This gives us a lightweight, fully reactive header animation.


📦 Collapsible Header Component

struct CollapsibleHeader<Content: View>: View {
    let height: CGFloat
    @ViewBuilder var content: () -> Content
    @Binding var scrollOffset: CGFloat

    var body: some View {
        let progress = max(0, min(1, 1 - scrollOffset / 120))

        content()
            .frame(height: height * progress)
            .opacity(progress)
            .offset(y: -scrollOffset * 0.4)
            .animation(.easeOut(duration: 0.2), value: progress)
    }
}
Enter fullscreen mode Exit fullscreen mode
struct ExampleView: View {
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        ScrollView {
            GeometryReader { geo in
                Color.clear
                    .preference(
                        key: OffsetKey.self,
                        value: geo.frame(in: .named("scroll")).minY
                    )
            }
            .frame(height: 0)

            VStack(spacing: 16) {
                ForEach(0..<30) { i in
                    Text("Row \(i)")
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .background(.ultraThinMaterial)
                        .clipShape(RoundedRectangle(cornerRadius: 12))
                }
            }
            .padding()
        }
        .coordinateSpace(name: "scroll")
        .onPreferenceChange(OffsetKey.self) { value in
            scrollOffset = value
        }
        .overlay(alignment: .top) {
            CollapsibleHeader(height: 140, scrollOffset: $scrollOffset) {
                VStack {
                    Text("Dashboard")
                        .font(.largeTitle.bold())
                    Text("Smooth collapsible header")
                        .foregroundColor(.secondary)
                }
                .frame(maxWidth: .infinity)
            }
        }
    }
}

struct OffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
Enter fullscreen mode Exit fullscreen mode

✔️ Final Result

You now have a reusable, modern collapsible header that feels native and smooth — perfect for:

  • dashboards
  • profile screens
  • lists
  • content-heavy apps

It’s clean, lightweight, and uses only pure SwiftUI.

Top comments (0)