DEV Community

Sebastien Lato
Sebastien Lato

Posted on • Edited on

How to Build a Floating Bottom Sheet in SwiftUI (Drag, Snap, Blur)

Floating bottom sheets are one of the cleanest modern UI patterns in iOS.

You see them in:

  • Apple Maps
  • Apple Music
  • Stocks
  • Reminders
  • Shortcuts

…and almost every modern iOS app.

In this tutorial, we’ll build a smooth, draggable, snapping, blur-backed bottom sheet using pure SwiftUI.

  • No UIKit.
  • No hacks.
  • No gesture bugs.
  • Just clean 2026-style SwiftUI.

🎯 What We'll Build

A bottom sheet that:

  • floats above your content
  • has a blur background
  • drags smoothly with your finger
  • snaps to three positions (min / mid / full)
  • has a soft spring animation
  • supports scrollable content inside

Perfect for dashboards, media players, maps, tools, or any modern app section.


🧠 Key Concept

We track:

@State private var offset: CGFloat = 0
@State private var lastDrag: CGFloat = 0
Enter fullscreen mode Exit fullscreen mode

Then we:

  • Apply a DragGesture()
  • Calculate the drag distance
  • Snap to the closest position on gesture end
  • Animate using .spring(...)

This creates a natural, Apple-like feel.


📦 Bottom Sheet Component

import SwiftUI

struct BottomSheet<Content: View>: View {
    let maxHeight: CGFloat
    let midHeight: CGFloat
    let minHeight: CGFloat
    @ViewBuilder var content: () -> Content

    @State private var offset: CGFloat = 0
    @State private var lastDrag: CGFloat = 0

    var body: some View {
        let drag = DragGesture()
            .onChanged { value in
                let newOffset = lastDrag + value.translation.height
                if newOffset > 0 && newOffset < (maxHeight - minHeight) {
                    offset = newOffset
                }
            }
            .onEnded { value in
                let newOffset = lastDrag + value.translation.height

                // Determine snap point
                let snap: CGFloat
                let top = 0.0
                let middle = maxHeight - midHeight
                let bottom = maxHeight - minHeight

                if newOffset < middle / 2 {
                    snap = top
                } else if newOffset < (bottom + middle) / 2 {
                    snap = middle
                } else {
                    snap = bottom
                }

                withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
                    offset = snap
                    lastDrag = snap
                }
            }

        VStack {
            Spacer()

            VStack(spacing: 12) {
                // Handle bar
                RoundedRectangle(cornerRadius: 3)
                    .fill(Color.gray.opacity(0.4))
                    .frame(width: 40, height: 5)
                    .padding(.top, 8)

                content()
                    .padding(.horizontal)
                    .padding(.bottom, 20)
            }
            .frame(width: UIScreen.main.bounds.width,
                   height: maxHeight,
                   alignment: .top)
            .background(.ultraThinMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
            .offset(y: offset)
            .gesture(drag)
        }
        .ignoresSafeArea(edges: .bottom)
    }
}
Enter fullscreen mode Exit fullscreen mode

🖼 Example Usage

struct BottomSheetDemo: View {
    var body: some View {
        ZStack {
            LinearGradient(colors: [.blue, .indigo],
                           startPoint: .top,
                           endPoint: .bottom)
                .ignoresSafeArea()

            BottomSheet(
                maxHeight: 600,
                midHeight: 350,
                minHeight: 120
            ) {
                VStack(alignment: .leading, spacing: 20) {
                    Text("Floating Bottom Sheet")
                        .font(.title2.bold())

                    Text("This bottom sheet snaps smoothly between three positions and supports any content inside. Try dragging it!")
                        .foregroundColor(.secondary)
                        .font(.body)

                    ForEach(0..<10) { i in
                        Text("Item \(i)")
                            .padding()
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .background(.thinMaterial)
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

✔️ Final Result

You now have a fully interactive, snapping, floating bottom sheet with:

  • blur
  • spring animation
  • multiple snap points
  • scrollable content
  • pure SwiftUI

This is the same pattern used across modern system apps — and now you can drop it into any of your projects.

Top comments (0)