SwiftUI Navigation Migration for iOS 26: Tab Bars, Sidebars, and Glass Effects
If your app ships a TabView or NavigationSplitView, iOS 26 just changed the rules. Apple’s Liquid Glass redesign
doesn’t just restyle your tab bar — it introduces new layout behaviors, minimization semantics, and glass extension
APIs that demand deliberate adoption. Ship without updating and your navigation will look like a relic.
This guide walks through every navigation-level change in iOS 26: tab bar minimization, bottom accessories, glass
sidebar effects, and NavigationSplitView updates. We won’t cover the broader Liquid Glass design system (that’s
covered in Liquid Glass Design System) or toolbar-specific changes (see
SwiftUI Toolbars in iOS 26). This post assumes you’re comfortable with
navigation architecture in SwiftUI and
TabView fundamentals.
Contents
- The Problem
- Tab Bar Minimization with
tabBarMinimizeBehavior - Bottom Accessories with
tabViewBottomAccessory - Glass Sidebars with
backgroundExtensionEffect - Updated
NavigationSplitViewBehavior - Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Consider a standard Pixar movie browser app built with TabView and NavigationSplitView. On iOS 18, this code works
fine:
struct PixarStudioApp: View {
@State private var selectedTab: StudioTab = .movies
@State private var selectedMovie: Movie?
var body: some View {
TabView(selection: $selectedTab) {
Tab("Movies", systemImage: "film", value: .movies) {
NavigationSplitView {
MovieListView(selection: $selectedMovie)
} detail: {
MovieDetailView(movie: selectedMovie)
}
}
Tab("Characters", systemImage: "person.2", value: .characters) {
CharacterBrowserView()
}
Tab("Studios", systemImage: "building.2", value: .studios) {
StudioMapView()
}
}
}
}
Drop this into iOS 26 without changes and you’ll notice three things immediately:
- The tab bar renders with Liquid Glass but ignores new minimization behaviors, so it permanently occupies screen space even during immersive content like fullscreen movie trailers.
- No bottom accessory support. Media-rich apps like this one lose the ability to show a persistent mini-player or “now playing” strip beneath the tabs.
NavigationSplitViewsidebars don’t pick up the new glass extension effect, making them visually inconsistent with system apps like Photos and Music.
Each of these requires explicit opt-in. Let’s address them one by one.
Tab Bar Minimization with tabBarMinimizeBehavior
iOS 26 introduces
tabBarMinimizeBehavior — a
modifier that controls when and how the tab bar collapses into a compact floating pill. System apps use this
extensively: Photos minimizes the tab bar during fullscreen photo review, and Music minimizes it during playback.
The API offers several behaviors:
struct PixarStudioApp: View {
@State private var selectedTab: StudioTab = .movies
var body: some View {
TabView(selection: $selectedTab) {
Tab("Movies", systemImage: "film", value: .movies) {
MovieBrowserView()
}
Tab("Characters", systemImage: "person.2", value: .characters) {
CharacterBrowserView()
}
}
.tabBarMinimizeBehavior(.onScrollDown) // ← Minimizes when user scrolls down
}
}
The .onScrollDown behavior tracks the first scrollable content within each tab and collapses the tab bar when the user
scrolls downward. Scrolling back up restores it. This is the most common behavior you’ll adopt — it’s what Safari and
Photos use.
Available Minimization Behaviors
.onScrollDown— Collapses the tab bar when scrollable content scrolls downward. Restores on upward scroll. Best for content-heavy browsing interfaces..automatic— Lets the system decide based on the content structure. Currently behaves like.onScrollDownfor most layouts..never— Disables minimization entirely. Use this when the tab bar must remain visible at all times (e.g., communication apps where tab badges are critical).
Programmatic Minimization
For scenarios where scroll-based minimization isn’t enough — say, when a user enters a fullscreen movie trailer — you
can drive minimization programmatically with the
TabBarMinimizability environment value:
struct MovieTrailerView: View {
@Environment(\.tabBarMinimizability) private var tabBarMinimizability
@State private var isFullscreen = false
var body: some View {
VideoPlayer(player: trailerPlayer)
.onTapGesture {
isFullscreen.toggle()
}
.onChange(of: isFullscreen) { _, fullscreen in
if fullscreen {
tabBarMinimizability.minimize()
} else {
tabBarMinimizability.restore()
}
}
}
}
Tip: Programmatic minimization requires that the parent
TabViewhas atabBarMinimizeBehaviorset to something other than.never. If you set.never, calls tominimize()are silently ignored.
Bottom Accessories with tabViewBottomAccessory
Media apps, ride-sharing apps, and anything with persistent contextual controls benefit from the new
tabViewBottomAccessory
modifier. It places a view below the tab bar that collapses and expands alongside tab bar minimization.
Here’s a “Now Playing” strip for our Pixar movie app:
struct PixarStudioApp: View {
@State private var selectedTab: StudioTab = .movies
@State private var nowPlaying: Movie? = Movie(title: "Coco", year: 2017)
var body: some View {
TabView(selection: $selectedTab) {
Tab("Movies", systemImage: "film", value: .movies) {
MovieBrowserView()
}
Tab("Characters", systemImage: "person.2", value: .characters) {
CharacterBrowserView()
}
}
.tabBarMinimizeBehavior(.onScrollDown)
.tabViewBottomAccessory { // ← Persistent bottom accessory
if let movie = nowPlaying {
NowPlayingStrip(movie: movie)
}
}
}
}
struct NowPlayingStrip: View {
let movie: Movie
var body: some View {
HStack {
Image(systemName: "film.fill")
Text(movie.title)
.font(.subheadline.weight(.medium))
Spacer()
Button("Resume", systemImage: "play.fill") {
// Resume playback
}
.labelStyle(.iconOnly)
}
.padding(.horizontal)
}
}
The bottom accessory automatically inherits the Liquid Glass styling of the tab bar. When the tab bar minimizes, the accessory collapses into the minimized pill, and a tap on the pill restores both.
Sizing and Layout
The accessory content receives a constrained proposal — Apple recommends keeping it to a single row of compact
controls. Overflowing content gets clipped, not scrolled. For richer expanded states, use a .sheet triggered from the
accessory rather than trying to pack everything into the strip itself.
Warning: Don’t place interactive scroll views inside a bottom accessory. The gesture system will conflict with the tab bar’s own minimization gestures, producing unpredictable results.
Glass Sidebars with backgroundExtensionEffect
One of the most visually impactful iOS 26 changes is the glass treatment on sidebars. System apps like Music and Files
use
backgroundExtensionEffect to
extend the Liquid Glass material across the full sidebar surface, creating a translucent, depth-aware panel.
Without this modifier, your NavigationSplitView sidebar renders with the old opaque background — a jarring mismatch
next to system apps that have adopted the glass treatment.
struct MovieBrowserView: View {
@State private var selectedMovie: Movie?
@State private var columnVisibility: NavigationSplitViewVisibility = .all
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
MovieSidebarList(selection: $selectedMovie)
.backgroundExtensionEffect(.sidebar) // ← Glass sidebar treatment
} detail: {
if let movie = selectedMovie {
MovieDetailView(movie: movie)
} else {
ContentUnavailableView(
"Select a Movie",
systemImage: "film",
description: Text("Pick a Pixar classic from the sidebar.")
)
}
}
}
}
The .sidebar variant applies the standard system sidebar glass material. It respects the user’s transparency and
accessibility settings — when Reduce Transparency is enabled, the sidebar falls back to a solid tinted background
automatically.
Three-Column Layouts
For three-column NavigationSplitView layouts, apply the effect to both the sidebar and the supplementary column:
NavigationSplitView(columnVisibility: $columnVisibility) {
StudioListView(selection: $selectedStudio)
.backgroundExtensionEffect(.sidebar)
} content: {
if let studio = selectedStudio {
MovieListView(studio: studio, selection: $selectedMovie)
.backgroundExtensionEffect(.sidebar) // ← Also glass for the middle column
}
} detail: {
MovieDetailView(movie: selectedMovie)
}
Note: The detail column does not receive glass treatment — it uses the standard content background. Applying
.backgroundExtensionEffect(.sidebar)to the detail column has no effect.
Updated NavigationSplitView Behavior
Beyond glass styling, NavigationSplitView in iOS 26 introduces behavioral changes that affect layout on both iPhone
and iPad.
Automatic Sidebar Resizing on iPad
iPadOS 26 allows users to resize sidebar columns by dragging the column divider. Your sidebar content should support
flexible widths. If you’ve hardcoded sidebar widths with .frame(width:), remove them or switch to
.frame(minWidth:idealWidth:maxWidth:):
MovieSidebarList(selection: $selectedMovie)
.backgroundExtensionEffect(.sidebar)
.frame(minWidth: 200, idealWidth: 280, maxWidth: 360) // ← Flexible, not fixed
Improved Compact Adaptation
On iPhone, NavigationSplitView collapses to a single NavigationStack. iOS 26 improves the animation of this
transition, but it also changes the default back-button behavior. The sidebar now uses the title from the
navigationTitle modifier as the back label, rather than “Back”. Make sure your sidebar has a meaningful title:
MovieSidebarList(selection: $selectedMovie)
.backgroundExtensionEffect(.sidebar)
.navigationTitle("Pixar Films") // ← This becomes the back label on iPhone
Column Visibility and Landscape
iOS 26 tweaks default column visibility in landscape on larger iPhones (Pro Max models). The sidebar now shows by default in landscape, matching iPad behavior. If this doesn’t suit your app — for instance, if your detail view needs the full width for a movie poster gallery — explicitly set the initial visibility:
@State private var columnVisibility: NavigationSplitViewVisibility = .detailOnly
Advanced Usage
Combining Tab Bar Minimization with Scroll Phase
For fine-grained control, combine tabBarMinimizeBehavior with the
ScrollPhaseReader to trigger side effects when
the tab bar state changes. This is useful for coordinating animations — such as fading in a floating action button when
the tab bar minimizes:
struct MovieBrowserView: View {
@Environment(\.tabBarMinimizability) private var tabBarMinimizability
@State private var showFAB = false
var body: some View {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(movies) { movie in
MoviePosterCard(movie: movie)
}
}
}
if showFAB {
FloatingAddButton()
.transition(.scale.combined(with: .opacity))
.padding()
}
}
.onChange(of: tabBarMinimizability.isMinimized) { _, minimized in
withAnimation(.snappy) {
showFAB = minimized
}
}
}
}
Custom Glass Shapes for Navigation Elements
When your sidebar contains custom section headers or grouped content, you can combine backgroundExtensionEffect with
glassEffect for layered glass
treatments:
struct StudioSectionHeader: View {
let studioName: String
var body: some View {
Text(studioName)
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.glassEffect(.regular, in: .rect(cornerRadius: 12))
}
}
Warning: Layering multiple glass effects creates compounding translucency. Profile on actual hardware — the visual result in the Simulator does not accurately represent the blur and tint behavior on device. More on this in Performance Considerations.
Persisting Minimization State
Tab bar minimization state does not persist across app launches by default. If your app needs to restore the minimized
state (rare, but relevant for media players), read the isMinimized property on launch and drive it from your own
persisted state:
@AppStorage("tabBarMinimized") private var wasMinimized = false
.onChange(of: tabBarMinimizability.isMinimized) { _, minimized in
wasMinimized = minimized
}
.onAppear {
if wasMinimized {
tabBarMinimizability.minimize()
}
}
Performance Considerations
Liquid Glass effects are GPU-composited, which means they’re broadly cheap in terms of CPU time — but they’re not free on the GPU side.
Glass Sidebar Rendering Cost
Each glass surface adds a blur pass to the render pipeline. On A15 and later chips, a single backgroundExtensionEffect
on a sidebar adds roughly 1-2ms of GPU time per frame during scrolling. On older devices (A14, A13), this can spike to
3-4ms, which matters if you’re targeting 120Hz on ProMotion displays.
Mitigation strategies:
- Avoid applying
glassEffectto individual list rows within a glass sidebar. The sidebar-level glass is sufficient. - Use
Listwith standard row styles rather than customVStack-based layouts inside glass sidebars.Listis optimized for this rendering path. - Profile with Instruments’ Core Animation template. Look for “Offscreen Passes” — each glass surface generates one.
Tab Bar Minimization Animations
The minimize/restore animation is a UIKit spring animation driven by the system. It performs well out of the box, but if
you’re coordinating additional SwiftUI animations via onChange(of: tabBarMinimizability.isMinimized), use .snappy or
.smooth spring presets rather than custom springs. Mismatched durations cause visual stuttering where the tab bar and
your custom animation finish at different times.
Bottom Accessory Performance
The bottom accessory view is re-rendered on every tab bar geometry change during minimization. Keep it lightweight:
- No
GeometryReaderinside the accessory. - No heavy image loading — use cached thumbnails or SF Symbols.
- Avoid triggering state changes from within the accessory body during the minimize transition.
Apple Docs:
tabViewBottomAccessory— SwiftUI
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Content browsing apps (photos, media, catalogs) | Use .tabBarMinimizeBehavior(.onScrollDown). Reclaims screen space during immersive browsing. |
| Media player with persistent controls | Use tabViewBottomAccessory for a mini-player strip. Pairs naturally with minimization. |
| iPad-first productivity apps with sidebars | Apply .backgroundExtensionEffect(.sidebar) to match system app visual consistency. |
| Utility apps with 2-3 tabs and no scrolling | Use .tabBarMinimizeBehavior(.never). Minimization adds nothing and may confuse users. |
| Apps supporting iOS 17 and earlier | Wrap all iOS 26 modifiers in if #available(iOS 26, *) checks. These APIs are not backward-compatible. |
| Deeply nested navigation (4+ levels) | Tab bar minimization can disorient users who lose track of their position. Test thoroughly with real user flows. |
Backward Compatibility
None of these APIs are available below iOS 26. Use @available checks at the call site:
extension View {
@ViewBuilder
func pixarTabBarMinimization() -> some View {
if #available(iOS 26, *) {
self.tabBarMinimizeBehavior(.onScrollDown)
} else {
self
}
}
@ViewBuilder
func pixarGlassSidebar() -> some View {
if #available(iOS 26, *) {
self.backgroundExtensionEffect(.sidebar)
} else {
self
}
}
}
This pattern keeps your TabView and NavigationSplitView code clean while gracefully degrading on older OS versions.
Summary
tabBarMinimizeBehaviorcontrols when the tab bar collapses into a floating pill. Use.onScrollDownfor most content apps; use programmaticminimize()/restore()for fullscreen experiences.tabViewBottomAccessoryadds a persistent strip below the tab bar that collapses and restores with it. Keep the content lightweight — one row of compact controls.backgroundExtensionEffect(.sidebar)applies the Liquid Glass material toNavigationSplitViewsidebars. Apply it to every sidebar column, but not the detail column.NavigationSplitViewgains user-resizable sidebars on iPad, improved compact adaptation on iPhone, and updated default column visibility in landscape.- Profile glass effects on hardware. Each glass surface adds a GPU blur pass. Avoid stacking
glassEffecton individual rows inside an already-glass sidebar.
For the full Liquid Glass visual language — button styles, morphing transitions, and GlassEffectContainer — see
Liquid Glass Design System. For toolbar-specific updates including spacers,
badges, and scroll edge blur, see SwiftUI Toolbars in iOS 26.