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

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:

  1. 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.
  2. 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.
  3. NavigationSplitView sidebars 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 .onScrollDown for 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 TabView has a tabBarMinimizeBehavior set to something other than .never. If you set .never, calls to minimize() 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 glassEffect to individual list rows within a glass sidebar. The sidebar-level glass is sufficient.
  • Use List with standard row styles rather than custom VStack-based layouts inside glass sidebars. List is 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 GeometryReader inside 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)

ScenarioRecommendation
Content browsing apps (photos, media, catalogs)Use .tabBarMinimizeBehavior(.onScrollDown). Reclaims screen space during immersive browsing.
Media player with persistent controlsUse tabViewBottomAccessory for a mini-player strip. Pairs naturally with minimization.
iPad-first productivity apps with sidebarsApply .backgroundExtensionEffect(.sidebar) to match system app visual consistency.
Utility apps with 2-3 tabs and no scrollingUse .tabBarMinimizeBehavior(.never). Minimization adds nothing and may confuse users.
Apps supporting iOS 17 and earlierWrap 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

  • tabBarMinimizeBehavior controls when the tab bar collapses into a floating pill. Use .onScrollDown for most content apps; use programmatic minimize()/restore() for fullscreen experiences.
  • tabViewBottomAccessory adds 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 to NavigationSplitView sidebars. Apply it to every sidebar column, but not the detail column.
  • NavigationSplitView gains 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 glassEffect on 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.