Skip to content

Scroll Fade Cards

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

Scroll Fade Cards, playing
Download .swift
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

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())
}
}
ParameterTypeDefault
items[Item]Required
spacingCGFloat16
minimumScaleDouble0.84
minimumOpacityDouble0.2
card(Item) -> CardRequired

Copy this code into your project, or download the file.

ScrollFadeCards.swift
// 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()
}