SwiftUI Toolbar in iOS 26: ToolbarItem, Placement, and Customization Guide

Master the SwiftUI toolbar in iOS 26. ToolbarItem placements, keyboard bars, Liquid Glass styling, VoiceOver, and cross-platform behavior on iPhone, iPad, and Mac.

SwiftUI Toolbar iOS 26: Complete Guide

Updated: September 16, 2026

The SwiftUI toolbar in iOS 26 is the declarative way to attach buttons, menus, titles, and keyboard accessories to a navigation-hosted view. You add one with the .toolbar { } modifier and populate it with ToolbarItem, ToolbarItemGroup, or ToolbarSpacer, choosing a ToolbarItemPlacement (like .topBarTrailing, .bottomBar, or .keyboard) so SwiftUI puts the item in the right spot on iPhone, iPad, Mac, and visionOS. Get placements right and the toolbar adapts across platforms without a single #if os.

  • The .toolbar { } modifier only renders when the view is inside a NavigationStack, NavigationSplitView, or a presented sheet with its own navigation host. That is the #1 reason a toolbar “doesn’t show.”
  • ToolbarItem(placement:) gives you full control; ToolbarItemGroup is a shortcut for multiple items in the same placement.
  • iOS 26 introduces ToolbarSpacer, richer toolbarBackground/toolbarVisibility APIs, and the Liquid Glass style bleeds into the top and bottom bars by default.
  • The .keyboard placement pairs with @FocusState to build a compact accessory bar above the keyboard, which is useful for “Next / Done” buttons in forms.
  • Every toolbar item needs a real accessibilityLabel when its only content is an SF Symbol; VoiceOver reads the label, not the glyph.
  • Use toolbarRole(.editor) on iPad and .principal for centered titles or segmented pickers. These are the placements developers most often reach for and most often use wrong.

SwiftUI toolbar basics in iOS 26

Honestly, after shipping enough of these, I’ll say it plainly: 80% of the “toolbar bug” tickets I’ve triaged were the same thing. A .toolbar { } modifier attached to a view that wasn’t inside a navigation host. SwiftUI needs a hosting bar chrome (the top navigation bar, the bottom bar, or a Mac window toolbar) to render items into. Without a NavigationStack, NavigationSplitView, or a sheet with its own navigation, the modifier compiles but disappears at runtime.

The minimal, correct pattern looks like this:

import SwiftUI

struct LibraryView: View {
    @State private var isAdding = false

    var body: some View {
        NavigationStack {
            List(0..<20) { i in
                Text("Chapter \(i + 1)")
            }
            .navigationTitle("Library")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        isAdding = true
                    } label: {
                        Label("Add chapter", systemImage: "plus")
                    }
                }
            }
        }
    }
}

Three details matter here: the toolbar is inside the navigation stack scope (attached to the List, not the stack itself), the button uses Label so SwiftUI can pick between icon-only and text-and-icon depending on placement, and the placement is explicit. Skipping the placement gives you .automatic, which SwiftUI resolves per-platform. Fine for simple apps, painful the moment you need consistency. For the primary navigation container that hosts these toolbars, my SwiftUI NavigationStack guide covers routing and deep-linking rules you’ll want alongside toolbar work.

ToolbarItem vs ToolbarItemGroup vs ToolbarSpacer

These three types cover almost every layout you’ll build. Here is how I decide between them:

Type When to use Placement scope iOS 26 notes
ToolbarItem A single control at a specific placement Per item Preferred; supports showsIndicator and identifier-based customization
ToolbarItemGroup 2+ controls that share the same placement Shared across the group Ideal for bottom bars; automatically inserts hairline separators on Mac
ToolbarSpacer Push items apart or align to a specific edge Inherits placement New in iOS 26; replaces the old Spacer() hack inside a ToolbarItemGroup(placement: .bottomBar)
ToolbarTitleMenu Attach a menu to the navigation title Bound to title Renders as the chevron affordance beside the title

A concrete comparison: five bottom-bar buttons with a flexible middle. Before iOS 26 you’d wedge a Spacer() between items in a ToolbarItemGroup and hope SwiftUI didn’t collapse it. Now you use ToolbarSpacer, which is a first-class toolbar type and behaves correctly across placements including Liquid Glass’ new floating bar geometry:

.toolbar {
    ToolbarItem(placement: .bottomBar) {
        Button("Reply", systemImage: "arrowshape.turn.up.left") { reply() }
    }
    ToolbarItem(placement: .bottomBar) {
        Button("Forward", systemImage: "arrowshape.turn.up.right") { forward() }
    }
    ToolbarSpacer(.flexible, placement: .bottomBar)
    ToolbarItem(placement: .bottomBar) {
        Button("Archive", systemImage: "archivebox") { archive() }
    }
}

Note the argument style on Button: iOS 17+ accepts a title and systemImage: directly, which is more compact than wrapping in a Label. VoiceOver still reads the string title as the accessibility label, so no extra work.

Every ToolbarItemPlacement, explained

Placements are where developers get lost, mostly because the same enum works across iPhone, iPad, and Mac with different visual results. iOS 26’s official ToolbarItemPlacement documentation lists over a dozen cases; these are the ones I actually use every week:

  • .topBarLeading / .topBarTrailing: leading/trailing side of the navigation bar. These respect the reading direction, so on Arabic and Hebrew layouts trailing renders on the left. It’s also why you should never use raw .navigationBarTrailing in new code.
  • .principal: the centre slot in the navigation bar. Great for a segmented Picker or a compact title with a badge.
  • .bottomBar: the toolbar under the safe area. In iOS 26 this floats above content with Liquid Glass by default.
  • .navigation: resolves to the back-button slot on iPhone and the sidebar toggle on iPad/Mac. Use when you want a “show sidebar” action that behaves correctly per-platform.
  • .primaryAction: semantic placement for the app’s main call to action (e.g. “Compose” in a mail app). SwiftUI decides where to render it, but the placement signals intent to accessibility tools.
  • .confirmationAction / .cancellationAction / .destructiveAction: semantic placements that are especially useful in sheets. Order and styling automatically match platform conventions (cancel-left, confirm-right on iOS; reversed on macOS sheets).
  • .keyboard: renders in the keyboard accessory bar, only when the keyboard is visible.
  • .status: the centre of the bottom bar. Used for “12 items selected” style text.
  • .topBarLeading(spacing:): iOS 26 accepts a spacing argument on leading/trailing placements when you need pixel-precise gaps.

The general rule I follow: prefer semantic placements (.primaryAction, .confirmationAction) inside sheets and modals, and positional placements (.topBarTrailing, .bottomBar) on primary content views. Semantic placements pay off the second you support Mac Catalyst or visionOS, because SwiftUI knows “confirm” belongs in a different physical spot per platform.

Why is my SwiftUI toolbar not showing?

The failure modes are surprisingly limited. Walking through them in order catches every case I’ve seen:

  1. No navigation host. The .toolbar modifier requires a NavigationStack or NavigationSplitView ancestor. A bare VStack won’t render one. If you’re inside a .sheet { }, the sheet content needs its own NavigationStack; the parent’s stack doesn’t bleed through.
  2. Toolbar attached to the wrong view. The modifier must be on a view inside the navigation scope, not on the NavigationStack itself. Move it one level down.
  3. Hidden by .toolbar(.hidden) upstream. Deep-linked screens sometimes hide the toolbar and forget to restore it. Search for .toolbar(.hidden across your project.
  4. Empty body. If every ToolbarItem is inside a conditional that evaluates to false, the toolbar collapses. iOS 26 no longer reserves height for an empty toolbar, so the whole bar disappears rather than showing an empty strip.
  5. Bottom-bar without content. The .bottomBar placement requires at least one ToolbarItem to appear. Placing only a ToolbarSpacer is not enough.
  6. Presented in a .formSheet without navigation. The Liquid Glass form sheet strips the navigation chrome; if you want a toolbar inside, wrap in NavigationStack.

Building a keyboard toolbar with @FocusState

Keyboard toolbars are one of my favourite iOS 26 wins: they turn ugly “Next / Previous” forms into something that feels native without a single UITextInputAssistantItem. Combine ToolbarItemGroup(placement: .keyboard) with @FocusState (my @FocusState guide covers the focus chain patterns in depth), and you get a dismiss button plus field-hopping in a few lines:

struct SignupForm: View {
    enum Field: Hashable { case email, password, confirm }
    @FocusState private var focus: Field?
    @State private var email = ""
    @State private var password = ""
    @State private var confirm = ""

    var body: some View {
        Form {
            TextField("Email", text: $email)
                .focused($focus, equals: .email)
                .textContentType(.emailAddress)
                .keyboardType(.emailAddress)
            SecureField("Password", text: $password)
                .focused($focus, equals: .password)
                .textContentType(.newPassword)
            SecureField("Confirm password", text: $confirm)
                .focused($focus, equals: .confirm)
                .textContentType(.newPassword)
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                Button("Previous", systemImage: "chevron.up") { previous() }
                    .disabled(focus == .email)
                Button("Next", systemImage: "chevron.down") { next() }
                    .disabled(focus == .confirm)
                ToolbarSpacer(.flexible, placement: .keyboard)
                Button("Done") { focus = nil }
                    .fontWeight(.semibold)
            }
        }
    }

    private func previous() {
        switch focus {
        case .password: focus = .email
        case .confirm:  focus = .password
        default: break
        }
    }

    private func next() {
        switch focus {
        case .email:    focus = .password
        case .password: focus = .confirm
        default: break
        }
    }
}

Two practical tips. First, the “Done” button should always sit at the trailing edge, because that’s what users have learned since iOS 4. Setting focus = nil dismisses the keyboard without needing UIApplication.shared. Second, disable the previous/next buttons at the boundaries; letting them wrap around confuses VoiceOver users, who then hear the same field twice.

Customizing background, visibility, and title display

iOS 26 rebuilt the toolbar background API around Liquid Glass. Three modifiers cover 95% of what you need:

  • toolbarBackground(_ style:for:): sets a ShapeStyle for a specific bar. Common values are .thinMaterial, .regularMaterial, or a solid Color. Passing .hidden removes the material entirely, so content scrolls edge-to-edge under the bar.
  • toolbarBackgroundVisibility(_ visibility:for:): controls when the bar chrome appears. Use .automatic to let SwiftUI hide it while the scroll offset is at the top, or .visible to pin it.
  • toolbarTitleDisplayMode(_:): takes .inline, .large, or .inlineLarge. The new .inlineLarge is iOS 26 only and matches the Music app: inline while scrolled, large at the top.
ScrollView { ... }
    .navigationTitle("Now Playing")
    .toolbarTitleDisplayMode(.inlineLarge)
    .toolbarBackground(.regularMaterial, for: .navigationBar)
    .toolbarBackgroundVisibility(.automatic, for: .navigationBar)

A very common mistake: setting .toolbarBackground(.hidden) without also handling the status bar tint. Once the bar is gone, the system status bar reads from your view’s background. If you have a colored hero, apply .toolbarColorScheme(.dark, for: .navigationBar) or wrap the hero in a background that continues under the safe area.

toolbarRole and iPad-specific behaviour

The toolbarRole modifier is niche but powerful on iPad. It tells SwiftUI “this screen is an editor / a browser / a navigation stack” and the framework picks matching placements and behaviours. The three roles: .editor (Mail compose, Notes), .browser (Files, Photos album grid), and .navigationStack (default). Editor role, for example, moves your .principal item to the true centre of the wide iPad bar and collapses .topBarLeading/.topBarTrailing into a single spillover menu on narrow columns.

NavigationStack {
    ComposeView()
        .toolbarRole(.editor)
        .navigationTitle("New draft")
        .toolbar {
            ToolbarItem(placement: .cancellationAction) {
                Button("Cancel") { dismiss() }
            }
            ToolbarItem(placement: .confirmationAction) {
                Button("Send") { send() }
                    .fontWeight(.semibold)
            }
        }
}

Pair this with a NavigationSplitView and the toolbar automatically respects the sidebar / content / detail column boundaries, which means your Cancel and Send stay in the detail column’s bar and don’t leak into the sidebar. If you’re building multi-column iPad apps, my TabView with sidebar adaptation guide covers how the new tab-to-sidebar collapse interacts with toolbar placements.

Toolbar accessibility and VoiceOver

Toolbars are where accessibility rots first. Icon-only buttons with no explicit label read as “Button” in VoiceOver, and users get a bar of anonymous glyphs. Here are the rules I enforce on every project I ship:

  1. Every icon-only button needs a label. Use Button("Archive", systemImage: "archivebox"); that string is the accessibility label. If you use Image(systemName:) directly inside a Button, add .accessibilityLabel("Archive").
  2. Order matters for VoiceOver. Items are read in declaration order, not visual order, when placements resolve to the same physical spot. Keep your ToolbarItems in the same order you want them announced.
  3. Group related actions. Wrap a set of tools in a ToolbarItemGroup and add .accessibilityElement(children: .combine) so VoiceOver treats them as a single navigable unit when appropriate.
  4. Use hint text sparingly. .accessibilityHint("Archives the current message") is welcome when the label alone doesn’t make the outcome obvious; skip it when the label is a verb like “Archive.”
  5. Respect Dynamic Type. The toolbar chrome scales up to .accessibility3 automatically, but principal items with custom text should use Text with a scalable style, never a fixed font size.
  6. Test with the rotor. Enable the “Toolbars” rotor item in Settings → Accessibility → VoiceOver → Rotor. It’s the fastest way to notice an unlabelled button.

Apple’s Human Interface Guidelines for toolbars spell out the interaction expectations users bring to each placement, and the SwiftUI toolbar(content:) reference lists every overload if you need something exotic.

A full multi-platform example

So, here’s a screen I use as a template. It’s a reading list app that works on iPhone, iPad, and Mac, with a keyboard toolbar for the search field, a principal segmented picker, and a bottom bar with actions:

struct ReadingListView: View {
    enum Filter: String, CaseIterable { case all, unread, favorites }
    @State private var filter: Filter = .all
    @State private var search = ""
    @FocusState private var searchFocused: Bool

    var body: some View {
        NavigationStack {
            List { /* rows */ }
                .navigationTitle("Reading list")
                .toolbarTitleDisplayMode(.inlineLarge)
                .searchable(text: $search)
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        Picker("Filter", selection: $filter) {
                            ForEach(Filter.allCases, id: \.self) { f in
                                Text(f.rawValue.capitalized).tag(f)
                            }
                        }
                        .pickerStyle(.segmented)
                        .frame(maxWidth: 220)
                    }

                    ToolbarItemGroup(placement: .bottomBar) {
                        Button("Add", systemImage: "plus") { addArticle() }
                        ToolbarSpacer(.flexible, placement: .bottomBar)
                        Text("\(count) articles")
                            .foregroundStyle(.secondary)
                            .accessibilityAddTraits(.updatesFrequently)
                        ToolbarSpacer(.flexible, placement: .bottomBar)
                        Menu {
                            Button("Sort by date")   { sort(by: .date) }
                            Button("Sort by author") { sort(by: .author) }
                        } label: {
                            Label("More", systemImage: "ellipsis.circle")
                        }
                    }

                    ToolbarItemGroup(placement: .keyboard) {
                        ToolbarSpacer(.flexible, placement: .keyboard)
                        Button("Done") { searchFocused = false }
                    }
                }
        }
    }
}

Ship that on iPhone and you get a top navigation bar with a segmented picker in the middle, a bottom bar with two actions and an item counter, and a “Done” key above the keyboard while searching. Open the same app on iPad and the picker moves to the appropriate spot without any conditional code. Run on Mac Catalyst and the bottom bar collapses into the window toolbar. That’s the payoff of the declarative model: describe intent, get platform-appropriate chrome for free.

Frequently Asked Questions

How do I add a button to the toolbar in SwiftUI?

Attach a .toolbar { } modifier to a view inside a NavigationStack, then add a ToolbarItem(placement: .topBarTrailing) { Button(…) }. Use Button(“Add”, systemImage: “plus”) so VoiceOver reads a meaningful label.

Why is my SwiftUI toolbar not showing?

Nine times out of ten the view isn’t inside a NavigationStack or NavigationSplitView. The .toolbar modifier only renders when there is a navigation chrome to render into, so sheets and plain VStacks need their own navigation host.

What is the difference between ToolbarItem and ToolbarItemGroup?

ToolbarItem renders a single control at a specific placement. ToolbarItemGroup renders multiple controls that share one placement, which is convenient for bottom bars and keyboard toolbars where several items live side-by-side.

Can I customize the toolbar background in SwiftUI?

Yes. Use .toolbarBackground(_:for:) with a material like .regularMaterial or a solid Color, and pair it with .toolbarBackgroundVisibility(_:for:) to control when the chrome appears. In iOS 26 the default is Liquid Glass; pass a solid Color for a flat look.

How do I add a keyboard toolbar in SwiftUI?

Use ToolbarItemGroup(placement: .keyboard) inside your .toolbar { } modifier. Combine it with @FocusState so “Previous” and “Next” buttons can move focus between fields, and a “Done” button can set focus back to nil to dismiss the keyboard.

Ava Thompson
About the Author Ava Thompson

SwiftUI engineer focused on declarative animations and accessibility. Will fight you about navigation stacks.