Scroll To Top Button
A floating button that appears after scrolling down and returns the scroll view to the top.

- Navigation
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- scroll to top · back to top · floating button · scroll · scroll position · list · arrow · volver arriba · subir · botón flotante · desplazamiento · posición de scroll · lista · flecha
Description
Section titled “Description”A scroll view that shows a floating button after scrolling down, which returns to the top.
With Reduce Motion on, the button fades and the view jumps to the top.
struct ScrollToTopButtonDemo: View { private let colors: [Color] = [.indigo, .blue, .teal, .green, .orange, .pink]
var body: some View { ScrollToTopButton { LazyVStack(spacing: 14) { ForEach(0..<30, id: \.self) { index in HStack(spacing: 14) { Image(systemName: "photo.fill") .font(.title2) .foregroundStyle(.white) .frame(width: 60, height: 60) .background(colors[index % colors.count].gradient, in: .rect(cornerRadius: 14)) .accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) { Text("Story \(index + 1)") .font(.headline) Text("A short description of this story") .font(.subheadline) .foregroundStyle(.secondary) }
Spacer(minLength: 0) } .padding(12) .background(.regularMaterial, in: .rect(cornerRadius: 18)) } } .padding(16) } }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
threshold | CGFloat | 320 |
tint | Color | .indigo |
content | () -> Content | Required |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A scroll view that shows a floating button after scrolling down, which returns to the top.////// With Reduce Motion on, the button fades and the view jumps to the top.struct ScrollToTopButton<Content: View>: View { var threshold: CGFloat = 320 var tint: Color = .indigo @ViewBuilder var content: Content
@State private var position = ScrollPosition(edge: .top) @State private var isVisible = false @ScaledMetric(relativeTo: .title3) private var buttonSize = 52.0 @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { ScrollView { content } .scrollPosition($position) .onScrollGeometryChange(for: Bool.self) { geometry in geometry.contentOffset.y + geometry.contentInsets.top > threshold } action: { _, isPastThreshold in withAnimation(reduceMotion ? .easeInOut(duration: 0.2) : .spring(duration: 0.4, bounce: 0.35)) { isVisible = isPastThreshold } } .overlay(alignment: .bottomTrailing) { if isVisible { button .padding(20) .transition(reduceMotion ? .opacity : .scale(scale: 0.4).combined(with: .opacity)) } } }
private var button: some View { Button { withAnimation(reduceMotion ? nil : .smooth(duration: 0.5)) { position.scrollTo(edge: .top) } } label: { Image(systemName: "arrow.up") .font(.title3.bold()) .foregroundStyle(ScrollToTopButtonContrastingForeground(background: tint)) .frame(width: buttonSize, height: buttonSize) .background(tint.gradient, in: .circle) .shadow(color: .black.opacity(0.25), radius: 10, y: 5) } .buttonStyle(.plain) .accessibilityLabel("Back to top") }}
/// White or black, whichever reads better on `background`. Black only on a/// light color, so the system's own colors keep white.nonisolated struct ScrollToTopButtonContrastingForeground: ShapeStyle { let background: Color
func resolve(in environment: EnvironmentValues) -> Color { let color = background.resolve(in: environment) let luminance = 0.2126 * color.linearRed + 0.7152 * color.linearGreen + 0.0722 * color.linearBlue return luminance > 0.5 ? .black : .white }}
// MARK: - Demo
struct ScrollToTopButtonDemo: View { private let colors: [Color] = [.indigo, .blue, .teal, .green, .orange, .pink]
var body: some View { ScrollToTopButton { LazyVStack(spacing: 14) { ForEach(0..<30, id: \.self) { index in HStack(spacing: 14) { Image(systemName: "photo.fill") .font(.title2) .foregroundStyle(.white) .frame(width: 60, height: 60) .background(colors[index % colors.count].gradient, in: .rect(cornerRadius: 14)) .accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) { Text("Story \(index + 1)") .font(.headline) Text("A short description of this story") .font(.subheadline) .foregroundStyle(.secondary) }
Spacer(minLength: 0) } .padding(12) .background(.regularMaterial, in: .rect(cornerRadius: 18)) } } .padding(16) } }}
#Preview { ScrollToTopButtonDemo()}