Bogdan P.

Hi there! I'm Bogdan's digital clone - nice to meet you! I can answer questions about Bogdan and his work, and put you in touch with him if need be. What's on your mind?

Ensuring smooth liquid glass toolbar transitions with SwiftUI

SwiftUIiOSAnimationAppleLiquid GlasstoolbarNavigationStackNavigationSplitView

Apple's liquid glass effect brings some very nice toolbar transitions when switching between views. These can be seen throughout iOS 26:

Recreating them in Swift UI is fairly straightforward - all you need is the .toolbar()!

Imperative toolbars

I needed a dynamic toolbar that could update with different UI for various app sections. This required setting toolbars outside of navigation transitions. This can be done by updating the toolbar content in place, using withAnimation

Swift
1import SwiftUI
2
3enum ViewState {
4	case first
5	case second
6}
7
8struct ContentView: View {
9	@State private var currentView: ViewState = .first
10
11	var body: some View {
12		NavigationStack {
13			VStack {
14				Button("Toggle Toolbar") {
15					withAnimation { 
16						currentView = currentView == .first ? .second : .first
17					}
18				}
19				.buttonStyle(.borderedProminent)
20			}
21			.toolbar {
22				if currentView == .first {
23					ToolbarItemGroup(placement: .bottomBar) {
24						Button {
25							print("Heart button tapped")
26						} label: {
27							Label("Heart", systemImage: "heart.fill")
28						}
29
30						Button("Settings") {
31							print("Settings button tapped")
32						}
33
34						Spacer()
35
36						Button {
37							print("Star button tapped")
38						} label: {
39							Label("Star", systemImage: "star.circle.fill")
40						}
41
42						Button("D") {
43							currentView = .second
44						}
45					}
46				} else {
47					ToolbarItemGroup(placement: .bottomBar) {
48						Button {
49							print("Bookmark button tapped")
50						} label: {
51							Label("Bookmark", systemImage: "bookmark.fill")
52						}
53
54						Spacer()
55
56						Button("Share") {
57							print("Share button tapped")
58						}
59
60						Button {
61							print("Folder button tapped")
62						} label: {
63							Label("Folder", systemImage: "folder.fill")
64						}
65
66						Button("D") {
67							print("Second View Button D tapped")
68						}
69					}
70				}
71			}
72		}
73	}
74}
75
76#Preview {
77	ContentView()
78}
Preview

Common pitfall: The toolbar must be rendered within a NavigationStack. Without it, toolbar animations won't work and .bottomBar placement may be ignored entirely.

On navigation

The .toolbar() modifier is perfect for alignment and cross-platform compatibility. Nice transitions are automatically applied during navigation when new views are rendered with their own .toolbar() modifiers.

Swift
1import SwiftUI
2
3struct ContentView: View {
4    var body: some View {
5        NavigationStack {
6            FirstView()
7        }
8    }
9}
10
11struct FirstView: View {
12    var body: some View {
13        VStack {
14            Image(systemName: "house.fill")
15                .imageScale(.large)
16                .foregroundStyle(.tint)
17            Text("First View")
18                .font(.largeTitle)
19                .padding()
20            
21            NavigationLink("Go to Second View") {
22                SecondView()
23            }
24            .buttonStyle(.borderedProminent)
25        }
26        .navigationTitle("Home")
27        .toolbar {
28            ToolbarItemGroup(placement: .bottomBar) {
29                Button {
30                    print("Heart button tapped")
31                } label: {
32                    Label("Heart", systemImage: "heart.fill")
33                }
34
35                Button("Settings") {
36                    print("Settings button tapped")
37                }
38
39                Spacer()
40
41                Button {
42                    print("Star button tapped")
43                } label: {
44                    Label("Star", systemImage: "star.circle.fill")
45                }
46
47                Button("D") {
48                    print("Button D tapped")
49                }
50            }
51        }
52    }
53}
54
55struct SecondView: View {
56    var body: some View {
57        VStack {
58            Image(systemName: "star.fill")
59                .imageScale(.large)
60                .foregroundStyle(.tint)
61            Text("Second View")
62                .font(.largeTitle)
63                .padding()
64            
65            Text("This is one navigation level deep")
66                .foregroundStyle(.secondary)
67        }
68        .navigationTitle("Details")
69        .navigationBarTitleDisplayMode(.inline)
70        .toolbar {
71            ToolbarItemGroup(placement: .bottomBar) {
72                Button {
73                    print("Bookmark button tapped")
74                } label: {
75                    Label("Bookmark", systemImage: "bookmark.fill")
76                }
77
78                Spacer()
79
80                Button("Share") {
81                    print("Share button tapped")
82                }
83
84                Button {
85                    print("Folder button tapped")
86                } label: {
87                    Label("Folder", systemImage: "folder.fill")
88                }
89
90                Button("D") {
91                    print("Second View Button D tapped")
92                }
93            }
94        }
95    }
96}
97
98#Preview {
99    ContentView()
100}
Preview

Other approaches

As of writing this, the animation isn't perfect when using in place updates with withAnimation - some minor visual artifacts and inconsistencies can be seen, mostly when animating text. I've found that setting fixed frame dimensions helps enourmously. In any case, if you can't use the approach above, here are your alternatives:

  1. Real navigation. Push or pop so UIKit manages the container transition and the item swap for us.
  2. Control the system toolbar directly without navigation. Keep our SwiftUI view intact but set items using UIKit:
  3. Create a custom SwiftUI bar and animate it ourselves. Instead of using .toolbar, use an overlay and utilize the GlassEffectContainer to replicate the native behavior to some extent.

Conclusion

Animated toolbar transitions in SwiftUI work best when your view hierarchy includes a NavigationStack. This allows SwiftUI to leverage UIKit's built in animation system for liquid glass effects. While this approach has limitations, it provides the most straightforward path to achieving the toolbar animations seen throughout iOS.

If your app structure doesn't allow for this approach, consider the alternatives outlined above or stick with navigation based transitions where the animations work out of the box.