SwiftUI Bottom Toolbar and Placement API
SwiftUI's toolbar API does something both impressive and frustrating: the framework decides what to show and hide based on context you didn't ask it to consider.
You describe placement intent—"put this button top-right"—and SwiftUI interprets based on device, orientation, and available space. Sometimes it honors your intent. Sometimes it moves items to overflow menus. Sometimes it hides them entirely.
ToolbarItem Basics
SwiftUI's toolbar API uses a toolbar modifier with ToolbarItem elements. Each needs placement (where you want it) and content (what to show). Apple's ToolbarItemPlacement documentation lists placements like .topBarLeading, .topBarTrailing, .bottomBar, .status, .title, and .subtitle.
The simplest example:
import SwiftUI
struct BasicToolbarDemo: View {
var body: some View {
NavigationStack {
Text("Content goes here")
.navigationTitle("Basic Toolbar")
.toolbar {
// Single toolbar item in top-right
ToolbarItem(placement: .topBarTrailing) {
Button { } label: {
Image(systemName: "plus")
}
}
}
}
}
}
Key points:
- Must be inside NavigationStack - Toolbars attach to navigation bars
- placement parameter - Tells SwiftUI where you want it
- Content closure - What to display (usually a Button)
Common placements:
.topBarLeading- Top-left corner (menu, back button).topBarTrailing- Top-right corner (add, edit, share).bottomBar- Bottom toolbar area.status- Centered in bottom bar (non-interactive status text).title- Custom title view in navigation bar.subtitle- Subtitle text below title
The Four Corners Pattern
I wanted toolbar items in all four screen corners. Common in file browsers (Files app), editing apps (Pages, TextEdit), and photo apps (Photos). Here's the simplest version:
struct FourCornersDemo: View {
var body: some View {
NavigationStack {
// DemoData.generatePeople() creates 50 placeholder names
List(DemoData.generatePeople(), id: \.self) { person in
Text(person)
}
.navigationTitle("Four Corners")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button { } label: {
Image(systemName: "line.3.horizontal")
}
}
ToolbarItem(placement: .topBarTrailing) {
Button { } label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .bottomBar) {
Button { } label: {
Label("Back", systemImage: "chevron.backward")
}
}
ToolbarItem(placement: .bottomBar) {
Spacer()
}
ToolbarItem(placement: .bottomBar) {
Button { } label: {
Label("Next", systemImage: "chevron.forward")
}
}
}
}
}
}
Five ToolbarItems: top-left, top-right, bottom-left, a Spacer in the middle, and bottom-right. The Spacer pushes the bottom buttons to the edges.
Tip
Always test toolbar layouts on macOS if your app supports it. Bottom bar items disappear entirely on macOS because Mac windows have no bottom toolbar concept.
On iPhone, all four buttons appear where specified. On iPad, same buttons with more spacing. On macOS, the top buttons appear in the window toolbar, but bottom buttons disappear—macOS windows don't have bottom toolbars, so SwiftUI hides them.
Kitchen Sink Placements
SwiftUI has more toolbar placements beyond the four corners. Here's a kitchen sink example showing .title, .subtitle, and .status:
struct ToolbarKitchenSink: View {
@State private var selectedCount = 3
private let people = DemoData.generatePeople()
var body: some View {
NavigationStack {
List(people, id: \.self) { person in
Text(person)
}
.navigationTitle("Kitchen Sink")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button { } label: {
Image(systemName: "line.3.horizontal")
}
}
ToolbarItem(placement: .topBarTrailing) {
Button { } label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .title) {
HStack(spacing: 6) {
Image(systemName: "tray.fill")
Text("Inbox")
}
}
ToolbarItem(placement: .subtitle) {
Text("\(selectedCount) of \(people.count) people selected")
.foregroundStyle(.secondary)
}
ToolbarItem(placement: .status) {
Button { } label: {
Text("\(selectedCount) of \(people.count) people selected")
}
.controlSize(.large)
}
ToolbarItem(placement: .bottomBar) {
Button { } label: {
Label("Back", systemImage: "chevron.backward")
}
}
ToolbarItem(placement: .bottomBar) {
Button { } label: {
Label("Next", systemImage: "chevron.forward")
}
}
}
}
}
}
.title- Custom title view in navigation bar (replaces standard title).subtitle- Subtitle text below title.status- Centered in bottom bar (can be button or text)
Portrait-Only iPhone Apps Don't Need This
Here's my issue: if you're building a portrait-only iPhone app, SwiftUI's cross-platform adaptation solves a problem you don't have.
I want fixed button placement on a simple iPhone app. SwiftUI's toolbar API assumes universal apps that might run on iPad or macOS, rotate to landscape, and need adaptive layouts.
Apple's documentation doesn't make the constraints clear upfront. You discover them by testing. The docs say:
"In compact horizontal size classes, the system limits both the leading and trailing positions of the navigation bar to a single item each."
Translation: iPhone portrait gets one button in .topBarLeading and one button in .topBarTrailing. Want more? They go to overflow menus automatically.
Note
In compact horizontal size classes (iPhone portrait), SwiftUI limits leading and trailing toolbar positions to one item each. Extra items silently move to overflow menus.
Automatic Overflow with Too Many Items
What happens when you add 10 toolbar buttons? SwiftUI creates an overflow menu. I built a demo with a star button (left) and 10 action icons (right):
struct TenItemsTopTrailing: View {
@State private var isStarred = false
@State private var showAlert = false
@State private var alertMessage = ""
private let trailingIcons = [
"pencil", "trash", "square.and.arrow.up", "doc.on.doc",
"folder", "tag", "link", "clock", "bell", "flag"
]
var body: some View {
NavigationStack {
List(DemoData.generatePeople(), id: \.self) { person in
Text(person)
}
.navigationTitle("Lots of Actions")
.alert(alertMessage, isPresented: $showAlert) {
Button("OK") { }
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
isStarred.toggle()
alertMessage = isStarred ? "Starred" : "Unstarred"
showAlert = true
} label: {
Image(systemName: isStarred ? "star.fill" : "star")
.foregroundColor(isStarred ? .yellow : .primary)
}
}
ToolbarItemGroup(placement: .topBarTrailing) {
ForEach(trailingIcons, id: \.self) { icon in
Button {
alertMessage = "\(icon.capitalized) clicked"
showAlert = true
} label: {
Image(systemName: icon)
}
}
}
}
}
}
}
I didn't write code to create the overflow menu. SwiftUI saw 10 items, measured available space, and moved extras into a "..." button. You get no control over this. No API to customize it, no way to prioritize which items stay visible, no option to change the overflow UI.
On iPhone, 4-5 icons show before overflow. On iPad, 5-7. On macOS, all 10 might fit if the window is wide enough. SwiftUI picks which items get hidden based on its own logic. Want to keep the most important actions visible? You can't specify that.
Apple's Vision vs Developer Needs
Apple wants universal apps running on iPhone, iPad, macOS, and visionOS with consistent HIG adherence and automatic adaptation.
Many developers want a simple iPhone portrait app with exact button placement, predictable behavior, and no hidden overflow menus.
These goals conflict. SwiftUI's toolbar API optimizes for universal apps that adapt everywhere. For simple iPhone apps, it creates friction. The framework makes decisions for you, and you can't override them.
Related posts:
- Understanding SwiftUI's TabView - Learn how tab bars work with the iOS 26 Tab API
- SwiftUI Liquid Glass and Accessibility - Make your toolbars accessible with proper VoiceOver support