Data note
25

Data note · Published Oct 12, 2025 · Updated Aug 28, 2026

SwiftUI Toolbar Placement Is Intent, Not Coordinates

I tested SwiftUI toolbar placement, grouping, search, and overflow on iPhone and iPad. The useful mental model is intent—not fixed screen coordinates.

A field-guide drawing of a camping headlamp and star chart
In this article7 sections

I started this experiment by putting toolbar buttons in “four corners.” That phrase led me in the wrong direction.

SwiftUI does not treat a toolbar placement as a coordinate. A placement describes the role or region an item prefers, and the system resolves that request using the current container, platform, size, and available space. The same declaration can be visible, regrouped, or moved into overflow as those conditions change.

Apple’s ToolbarItemPlacement documentation makes the distinction explicit:

Placement kindExamplesWhat it communicates
Semantic.primaryAction, .confirmationAction, .statusWhat the item means
Explicit region.topBarLeading, .topBarTrailing, .bottomBarWhere the item prefers to appear

Even an explicit region is not a promise that ten controls will fit there.

Start with the Action’s Meaning

For a basic navigation-bar action, a trailing placement is clear:

struct BasicToolbarDemo: View {
    var body: some View {
        NavigationStack {
            ContentView()
                .navigationTitle("Inbox")
                .toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        Button("Add", systemImage: "plus") {
                            // Add an item
                        }
                    }
                }
        }
    }
}

The toolbar modifier itself does not universally require a NavigationStack. Toolbars can belong to navigation containers, sheets, windows, and other system presentations. The stack is necessary here because .topBarTrailing is intended for the navigation bar.

When the action has a semantic role, prefer that role over a visual guess. A sheet is the clearest example:

.sheet(isPresented: $showEditor) {
    NavigationStack {
        EditorView()
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") { dismiss() }
                }

                ToolbarItem(placement: .confirmationAction) {
                    Button("Save") { save() }
                }
            }
    }
}

The system can place and style those actions according to platform convention without the app hard-coding “left” and “right.”

A Bottom Bar Is a Layout Region

Here is the corrected version of my four-edge demo:

.toolbar {
    ToolbarItem(placement: .topBarLeading) {
        Button("Menu", systemImage: "line.3.horizontal") { }
    }

    ToolbarItem(placement: .topBarTrailing) {
        Button("Add", systemImage: "plus") { }
    }

    ToolbarItem(placement: .bottomBar) {
        Button("Back", systemImage: "chevron.backward") { }
    }

    ToolbarSpacer(.flexible, placement: .bottomBar)

    ToolbarItem(placement: .bottomBar) {
        Button("Next", systemImage: "chevron.forward") { }
    }
}

ToolbarSpacer is the iOS 26 API designed for spacing and visual grouping inside toolbars. It is better evidence of intent than wrapping a generic Spacer in another ToolbarItem.

This project targets iPhone and iPad, so those are the platforms the example proves. I previously claimed what the same code did on macOS without providing a macOS target or test. I have removed that claim. For a cross-platform app, the right approach is to run the same toolbar in each supported target rather than extrapolate from iOS.

.status Is for Information, Not a Center Button

My first kitchen-sink example put a Button in .status. That compiled, but it contradicted the placement’s documented meaning. A status item should report state rather than initiate an action.

ToolbarItem(placement: .status) {
    Text("3 of 50 selected")
        .font(.footnote)
}

On iOS and iPadOS, the system places status content in the center of the bottom toolbar. On macOS and Mac Catalyst, it places the status in the center of the toolbar. That is a good example of semantic placement adapting without losing its meaning.

The title-related placements are similarly specific:

  • .title supplies content in the navigation bar’s title area.
  • .subtitle supplies an inline subtitle.
  • .principal marks the principal item section and is useful for a custom inline title composition.

I would not combine all three in production. The lab’s kitchen-sink screen deliberately does so to make the collision behavior visible.

Overflow Is Dynamic, Not a Device Formula

The ten-action demo is useful because it breaks any illusion of fixed placement:

ToolbarItemGroup(placement: .topBarTrailing) {
    ForEach(actions) { action in
        Button(action.title, systemImage: action.symbol) {
            perform(action)
        }
    }
}

When the items do not fit, SwiftUI can create an overflow menu. The result depends on available width, neighboring controls, labels, platform, and the current SDK. My earlier article claimed fixed ranges such as “4–5 icons on iPhone.” Those numbers came from one run and should not have been presented as API behavior.

I keep a small number of primary actions visible and group secondary actions deliberately. If an action must live in a menu, I put it in a Menu instead of hoping it overflows. Newer SDKs also expose explicit overflow APIs, but availability still has to match the app’s deployment target.

One more subtlety: a labeled ToolbarItemGroup can collapse its content into a menu when space is constrained. That gives the system a meaningful label for the group instead of a pile of unrelated icons.

What iOS 26 Changes

iOS 26 gives system toolbars a Liquid Glass surface and groups related controls visually. Existing toolbar declarations adopt much of this appearance when rebuilt with the iOS 26 SDK. The app should describe relationships rather than paint its own glass behind the controls.

ToolbarSpacer creates intentional breaks between groups:

.toolbar {
    ToolbarItem(placement: .bottomBar) {
        FilterButton()
    }

    ToolbarSpacer(.flexible, placement: .bottomBar)

    DefaultToolbarItem(kind: .search, placement: .bottomBar)

    ToolbarSpacer(.fixed, placement: .bottomBar)

    ToolbarItem(placement: .bottomBar) {
        ComposeButton()
    }
}

Apple uses this pattern in its iOS 26 SwiftUI design walkthrough. On iPhone, toolbar search appears near the bottom for reachability; on iPad and Mac it moves to the top-trailing area. The exact presentation can also minimize based on space and context.

The same redesign adds automatic scroll-edge effects to keep floating controls legible. Extra custom backgrounds behind a system bar can interfere with that effect, so I would remove those before adding more styling.

The Rule I Use Now

I no longer ask, “How do I force this button into a corner?” I ask three questions:

  1. Is this a navigation, primary, confirmation, cancellation, destructive, or status item?
  2. Which actions must remain immediately visible, and which belong in a menu?
  3. What happens in compact width, regular width, and every platform the app actually supports?

SwiftUI’s toolbar system works well when the app supplies meaning and hierarchy. It becomes frustrating when I treat its placements as Auto Layout constraints. Placement is a request; the action’s role is the durable part.

Run the Examples

The SwiftUI tab and toolbar lab contains the basic, modal, bottom-bar, status, search, visibility, and overflow examples. It targets iOS 26 and includes iPhone and iPad destinations so I can compare the adaptive behavior directly. I test the same screens with the accessibility settings from my Liquid Glass notes before I trust the layout.

One quick signal

Did this earn your time?

What was missing?

Thanks. That gives me something concrete to check.