Scroll Fade Cards
A vertical scroll of cards that scale down and fade at its edges with scrollTransition.

- Scrolling
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- scroll transition · scroll · cards · fade · scale · vertical list · scroll effect · transición de scroll · desplazamiento · tarjetas · desvanecer · escala · lista vertical
Description
Section titled “Description”A vertical scroll of cards that shrink and fade as they reach its edges.
With Reduce Motion on, the cards fade without scaling.
private struct ScrollFadeDemoCard: Identifiable { let id: Int let title: String let systemImage: String let colors: [Color]}
struct ScrollFadeCardsDemo: View { private let cards: [ScrollFadeDemoCard] = { let themes: [(String, String, [Color])] = [ ("Ocean", "water.waves", [.blue, .cyan]), ("Sunrise", "sunrise.fill", [.orange, .pink]), ("Meadow", "leaf.fill", [.green, .mint]), ("Dusk", "moon.haze.fill", [.purple, .indigo]), ] return (0..<16).map { index in let theme = themes[index % themes.count] return ScrollFadeDemoCard(id: index, title: theme.0, systemImage: theme.1, colors: theme.2) } }()
@ScaledMetric(relativeTo: .title) private var cardHeight = 170.0
var body: some View { ScrollFadeCards(items: cards) { card in HStack(spacing: 16) { Image(systemName: card.systemImage) .font(.largeTitle) .accessibilityHidden(true)
Text(card.title) .font(.system(.title, design: .rounded, weight: .bold))
Spacer(minLength: 0) } .foregroundStyle(.white) .padding(.horizontal, 28) .frame(maxWidth: .infinity, minHeight: cardHeight) .background( LinearGradient(colors: card.colors, startPoint: .topLeading, endPoint: .bottomTrailing), in: .rect(cornerRadius: 26) ) } .background(Color.black.ignoresSafeArea()) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
items | [Item] | Required |
spacing | CGFloat | 16 |
minimumScale | Double | 0.84 |
minimumOpacity | Double | 0.2 |
card | (Item) -> Card | 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 vertical scroll of cards that shrink and fade as they reach its edges.////// With Reduce Motion on, the cards fade without scaling.struct ScrollFadeCards<Item: Identifiable, Card: View>: View { let items: [Item] var spacing: CGFloat = 16 var minimumScale: Double = 0.84 var minimumOpacity: Double = 0.2 @ViewBuilder var card: (Item) -> Card
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { // `scrollTransition` takes a `@Sendable` closure, so it works with local copies. let lowestScale = reduceMotion ? 1 : minimumScale let lowestOpacity = minimumOpacity
ScrollView { LazyVStack(spacing: spacing) { ForEach(items) { item in card(item) .scrollTransition(.interactive.threshold(.visible(0.9))) { content, phase in let distance = abs(phase.value) return content .scaleEffect(1 - distance * (1 - lowestScale)) .opacity(1 - distance * (1 - lowestOpacity)) } } } .padding(.horizontal, 20) } }}
// MARK: - Demo
private struct ScrollFadeDemoCard: Identifiable { let id: Int let title: String let systemImage: String let colors: [Color]}
struct ScrollFadeCardsDemo: View { private let cards: [ScrollFadeDemoCard] = { let themes: [(String, String, [Color])] = [ ("Ocean", "water.waves", [.blue, .cyan]), ("Sunrise", "sunrise.fill", [.orange, .pink]), ("Meadow", "leaf.fill", [.green, .mint]), ("Dusk", "moon.haze.fill", [.purple, .indigo]), ] return (0..<16).map { index in let theme = themes[index % themes.count] return ScrollFadeDemoCard(id: index, title: theme.0, systemImage: theme.1, colors: theme.2) } }()
@ScaledMetric(relativeTo: .title) private var cardHeight = 170.0
var body: some View { ScrollFadeCards(items: cards) { card in HStack(spacing: 16) { Image(systemName: card.systemImage) .font(.largeTitle) .accessibilityHidden(true)
Text(card.title) .font(.system(.title, design: .rounded, weight: .bold))
Spacer(minLength: 0) } .foregroundStyle(.white) .padding(.horizontal, 28) .frame(maxWidth: .infinity, minHeight: cardHeight) .background( LinearGradient(colors: card.colors, startPoint: .topLeading, endPoint: .bottomTrailing), in: .rect(cornerRadius: 26) ) } .background(Color.black.ignoresSafeArea()) }}
#Preview { ScrollFadeCardsDemo()}