Issue #1061
Starting with iOS 26, scroll views automatically blur their content where it slides behind a toolbar, tab bar, or any other pinned control. This is the scroll edge effect, and it’s part of the new Liquid Glass design language. Apple picks a sensible default for you, but there are times when the automatic choice does not match your design, or you want no effect at all. SwiftUI gives you two modifiers to take control: scrollEdgeEffectStyle(_:for:) to pick a style, and scrollEdgeEffectHidden(_:for:) to turn the effect off entirely.
What the scroll edge effect actually does
When content scrolls underneath a bar, the system needs some way to keep that bar legible. Before iOS 26, this was often solved with a plain background material behind the bar. Now the system renders a graded effect at the edge of the scroll view, so text and controls stay readable while content passes behind them.
ScrollEdgeEffectStyle exposes three cases for this:
.automatic, which lets the system decide based on platform and context.hard, a nearly opaque, sharply defined boundary.soft, a gradual blur that fades in as content approaches the edge
You apply a style with scrollEdgeEffectStyle(_:for:), targeting one or more edges through an Edge.Set.
A minimal example
Here’s a scroll view with a bottom toolbar, using the hard style only at the bottom edge:
import SwiftUI
struct ArticleList: View {
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 12) {
ForEach(1...30, id: \.self) { index in
Text("Article \(index)")
.padding(.horizontal)
}
}
}
.scrollEdgeEffectStyle(.hard, for: .bottom)
.safeAreaBar(edge: .bottom) {
HStack {
Button("Filter", systemImage: "line.3.horizontal.decrease") {}
Spacer()
Button("Sort", systemImage: "arrow.up.arrow.down") {}
}
.padding()
}
}
}
As the list scrolls, rows pass behind the toolbar with a crisp, defined edge instead of the softer default blur. Swap .hard for .soft and you get a gentler fade instead, useful when the bar sits over busy or colorful content and a hard line looks too abrupt.
If you want the same style on every edge, pass .all instead of a single edge:
.scrollEdgeEffectStyle(.soft, for: .all)
The modifier applies to every ScrollView inside the view hierarchy where you place it, so you can set it once near the root of a screen rather than repeating it on each scroll view.
Turning the effect off
Sometimes you don’t want any transition at all, for example when your bar already has its own background or when the blur clashes with a custom look. Use scrollEdgeEffectHidden(_:for:) for that:
ScrollView {
LazyVStack {
ForEach(items) { item in
RowView(item: item)
}
}
}
.scrollEdgeEffectHidden(true, for: .top)
This removes the effect entirely for the edges you specify, rather than just changing its appearance. The parameter defaults to true and .all, so .scrollEdgeEffectHidden() on its own disables every edge.
UIKit and AppKit equivalents
If you’re bridging between SwiftUI and UIKit, or maintaining a UIKit screen directly, the same behavior is available through UIScrollView. Each edge has its own UIScrollEdgeEffect object, accessible via topEdgeEffect, bottomEdgeEffect, leftEdgeEffect, and rightEdgeEffect. Each effect exposes a style property, using UIScrollEdgeEffect.Style, and an isHidden flag:
scrollView.bottomEdgeEffect.style = .hard
scrollView.topEdgeEffect.isHidden = true
On macOS, NSScrollView doesn’t expose the same per-edge object model directly, but related controllers do. NSTitlebarAccessoryViewController and NSSplitViewItemAccessoryViewController both have a preferredScrollEdgeEffectStyle property, backed by NSScrollEdgeEffectStyle, which lets you request .automatic, .hard, or .soft for content scrolling behind a titlebar accessory or split view accessory.
Across all three frameworks the concept stays the same: a scroll view can tell you, per edge, whether an effect is applied and how strong it looks. SwiftUI just wraps it in two focused modifiers instead of separate objects and properties.
Start the conversation