Ensuring smooth liquid glass toolbar transitions with SwiftUI
Apple's liquid glass effect brings some very nice toolbar transitions when switching between views. These can be seen throughout iOS 26:
There are a few ways of implementing it in SwiftUI, the one you will pick depends on your use case and requirements.
`.toolbar()` without navigation
The `.toolbar()` modifier is the standard way of implementing toolbars in SwiftUI. You can get basic Liquid Glass toolbar transitions and functionality this way. iOS will automatically animate the transitions when navigating between views, as shown below - for in-view transitions, you will need to use the `withAnimation` modifier.
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}Common pitfall: The toolbar should be rendered within a NavigationStack. Without it, toolbar animations may not work and .bottomBar placement may be ignored entirely.
Beyond that, .toolbar(.bottomBar) has been historically buggy - items disappearing after navigation, toolbars failing to reappear on pop, and outright broken behavior that persists to this day. If your design calls for a bottom bar, I'd recommend implementing a custom one instead (shown below) rather than fighting the system toolbar.
`.toolbar()` during navigation
This is the standard transition you will encounter in the Liquid Glass design system. It is automatically applied whenever you push or pop a new view in a NavigationStack
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}Custom toolbar with GlassEffectContainer
The .toolbar() API is a fairly constrained interface. It works well for displaying standard buttons and menus, but falls short when you need more complex UI like floating text inputs, expandable menus, or context-aware toolbar states that change shape entirely.
For these cases, building a custom bottom bar using GlassEffectContainer, .glassEffect(), and .glassEffectUnion() gives you full control while preserving native liquid glass aesthetics. Combined with withAnimation(), you get smooth morphing transitions between toolbar states.
The pattern is: using an overlay and a GlassEffectContainer, conditionally render different toolbar contents based on state, and use .glassEffectUnion(id:namespace:) with a shared @Namespace to define the relationships between the glass elements. For a more in depth guide on GlassEffectContainer and related APIs, I highly recommend this article by itsuki. Individual buttons get .glassEffect(.regular.interactive()) for the native glass look (you can also use .buttonStyle(.glass) but that doesn not seem to play well with .glassEffectUnion in my experience). Here is a complete working example that demonstrates three toolbar states: collapsed (single button), expanded (full menu), and an inline edit bar with a text input:
1import SwiftUI
2
3@main
4struct GlassToolbarDemoApp: App {
5 var body: some Scene {
6 WindowGroup {
7 ContentView()
8 }
9 }
10}
11
12struct Item: Identifiable {
13 let id = UUID().uuidString
14 var title: String
15}
16
17struct ContentView: View {
18 @Namespace private var toolbarNS
19 @State private var menuExpanded = false
20 @State private var editingItemId: String?
21 @State private var editingTitle = ""
22 @State private var showAlternate = false
23 @FocusState private var isEditFocused: Bool
24
25 @State private var items: [Item] = [
26 .init(title: "First item"),
27 .init(title: "Second item"),
28 .init(title: "Third item"),
29 .init(title: "Fourth item"),
30 .init(title: "Fifth item"),
31 ]
32
33 var body: some View {
34 List {
35 ForEach(items) { item in
36 Button {
37 editingTitle = item.title
38 withAnimation(.snappy) { editingItemId = item.id }
39 } label: {
40 HStack {
41 Text(item.title)
42 .foregroundStyle(.primary)
43 Spacer()
44 if editingItemId == item.id {
45 Image(systemName: "pencil.line")
46 .foregroundStyle(.secondary)
47 }
48 }
49 }
50 }
51
52 Section {
53 Toggle("Alternate toolbar", isOn: Binding(
54 get: { showAlternate },
55 set: { newValue in
56 withAnimation(.snappy) { showAlternate = newValue }
57 }
58 ))
59 }
60 }
61 .contentMargins(.bottom, 80)
62 .overlay(alignment: .bottom) {
63 bottomBar
64 .padding(.horizontal, 20)
65 .padding(.bottom, 4)
66 }
67 }
68
69 // MARK: - Bottom Bar
70
71 @ViewBuilder
72 private var bottomBar: some View {
73 GlassEffectContainer {
74 if editingItemId != nil {
75 editBar
76 } else if menuExpanded {
77 expandedMenu
78 } else {
79 collapsedBar
80 }
81 }
82 }
83
84 private var collapsedBar: some View {
85 HStack {
86 Button {
87 withAnimation(.snappy) { menuExpanded = true }
88 } label: {
89 Image(systemName: "line.3.horizontal")
90 .foregroundStyle(.foreground)
91 .frame(width: 44, height: 44)
92 }
93 .glassEffect(.regular.interactive())
94 .glassEffectUnion(id: "toolbar", namespace: toolbarNS)
95
96 if showAlternate {
97 Button {} label: {
98 Image(systemName: "bookmark")
99 .foregroundStyle(.foreground)
100 .frame(width: 44, height: 44)
101 }
102 .glassEffect(.regular.interactive())
103
104 Button {} label: {
105 Image(systemName: "heart")
106 .foregroundStyle(.foreground)
107 .frame(width: 44, height: 44)
108 }
109 .glassEffect(.regular.interactive())
110 }
111
112 Spacer()
113
114 Menu {
115 Button {} label: {
116 Label("Option A", systemImage: "star")
117 }
118 Button {} label: {
119 Label("Option B", systemImage: "folder")
120 }
121 Button {} label: {
122 Label("Option C", systemImage: "archivebox")
123 }
124 } label: {
125 Image(systemName: "plus")
126 .foregroundStyle(.foreground)
127 .font(.system(size: 20))
128 .frame(width: 44, height: 44)
129 }
130 .glassEffect(.regular.interactive())
131 }
132 }
133
134 private var expandedMenu: some View {
135 HStack(spacing: 0) {
136 tabButton("house.fill")
137 tabButton("chart.bar")
138 tabButton("clock")
139 tabButton("gearshape")
140 }
141 .frame(maxWidth: .infinity)
142 }
143
144 private func tabButton(_ icon: String) -> some View {
145 Button {
146 withAnimation(.snappy) { menuExpanded = false }
147 } label: {
148 Image(systemName: icon)
149 .foregroundStyle(.primary)
150 .frame(width: 44, height: 44)
151 }
152 .buttonStyle(.glass)
153 .glassEffectUnion(id: "toolbar", namespace: toolbarNS)
154 }
155
156 private var editBar: some View {
157 HStack(spacing: 8) {
158 Image(systemName: "pencil.line")
159 .font(.subheadline)
160 .foregroundStyle(.primary)
161 TextField("Title", text: $editingTitle)
162 .font(.title3)
163 .focused($isEditFocused)
164 .submitLabel(.done)
165 .onSubmit { commitEdit() }
166 }
167 .padding(.horizontal, 12)
168 .padding(.vertical, 12)
169 .glassEffectUnion(id: "toolbar", namespace: toolbarNS)
170 .glassEffect(.regular.interactive())
171 .onChange(of: isEditFocused) { _, focused in
172 if !focused {
173 withAnimation(.snappy) { editingItemId = nil }
174 }
175 }
176 }
177
178 private func commitEdit() {
179 if let id = editingItemId,
180 let idx = items.firstIndex(where: { $0.id == id }) {
181 items[idx].title = editingTitle
182 }
183 withAnimation(.snappy) { editingItemId = nil }
184 }
185}
186
187#Preview {
188 ContentView()
189}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.