BS
BleepingSwift
Published on
Updated Nov 5, 2026
Tested on Xcode 27.1
8 min read

> Managing Keyboard Toolbar Items in SwiftUI (iOS 15+)

Share:

To add buttons above the keyboard in SwiftUI, use .toolbar { ToolbarItemGroup(placement: .keyboard) { ... } } on iOS 15 or later, and pair it with @FocusState so a Done button can dismiss the keyboard. You don't need UIKit's inputAccessoryView for it.

I ran every example below in the sample app on an iPhone 17 simulator running iOS 27.0, built with Xcode 27.1, and drove it with a UI test that typed into each field and tapped each toolbar button. The screenshots in this post come from that run.

What you wantHow it behaved on iOS 27.0
Done buttonfocusedField = nil dismissed the keyboard and the toolbar together
Buttons that depend on the focused fieldContents switched as soon as focus moved between fields
Previous / NextSetting @FocusState moved focus; .disabled greyed the chevron at each end
Live values (counter)A Text in the toolbar updated on every keystroke

What it looks like on iOS 27

If you remember the old full-width grey bar above the keyboard, the current look is different. On iOS 27.0 there's no bar at all: each group of items floats above the keyboard as its own rounded glass capsule. A Spacer() splits items into separate capsules on the left and right, and buttons placed next to each other share one capsule.

The Done button floating above the keyboard in a glass capsule

The Traditional Problem

Before iOS 15, adding buttons above the keyboard required either:

  • Using UITextField or UITextView with inputAccessoryView (UIKit bridge)
  • Complex positioning with keyboard notification observers
  • Third-party libraries

All of these approaches were cumbersome and didn't feel native to SwiftUI.

The Modern Solution: Keyboard Toolbar

iOS 15+ provides .toolbar with .keyboard placement:

Swift
import SwiftUI

struct KeyboardToolbarExample: View {
    @State private var text = ""
    @FocusState private var isFocused: Bool

    var body: some View {
        TextEditor(text: $text)
            .focused($isFocused)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer()

                    Button("Done") {
                        isFocused = false
                    }
                    .bold()
                }
            }
            .padding()
    }
}

How It Works

The keyboard toolbar uses SwiftUI's toolbar system:

  1. .toolbar: Declares toolbar items for the view
  2. ToolbarItemGroup(placement: .keyboard): Places items above the keyboard
  3. @FocusState: Tracks and controls keyboard focus
  4. Automatic animations: SwiftUI handles appearance/dismissal

The toolbar only appears when a focused text field brings up the keyboard.

Done Button Pattern

The most common use case - dismissing the keyboard:

Swift
struct DoneButtonExample: View {
    @State private var email = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?

    enum Field {
        case email, password
    }

    var body: some View {
        Form {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)
                .textContentType(.emailAddress)
                .autocapitalization(.none)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
                .textContentType(.password)
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                Spacer()

                Button("Done") {
                    focusedField = nil
                }
                .foregroundColor(.blue)
            }
        }
    }
}

Multiple Toolbar Items

Add several buttons to the keyboard toolbar:

Swift
struct RichTextEditor: View {
    @State private var text = ""
    @FocusState private var isFocused: Bool

    var body: some View {
        VStack {
            TextEditor(text: $text)
                .focused($isFocused)
                .border(Color.gray.opacity(0.3), width: 1)
                .frame(height: 200)
                .padding()

            Spacer()
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                // Formatting buttons
                Button {
                    insertMarkdown("**")
                } label: {
                    Image(systemName: "bold")
                }

                Button {
                    insertMarkdown("*")
                } label: {
                    Image(systemName: "italic")
                }

                Button {
                    insertMarkdown("`")
                } label: {
                    Image(systemName: "chevron.left.forwardslash.chevron.right")
                }

                Spacer()

                // Done button
                Button("Done") {
                    isFocused = false
                }
                .bold()
            }
        }
    }

    func insertMarkdown(_ marker: String) {
        text += marker
    }
}

Field-Specific Toolbars

Show different toolbar buttons based on which field is focused:

Swift
struct ConditionalToolbar: View {
    @State private var title = ""
    @State private var amount = ""
    @State private var notes = ""
    @FocusState private var focusedField: Field?

    enum Field {
        case title, amount, notes
    }

    var body: some View {
        Form {
            Section("Expense Details") {
                TextField("Title", text: $title)
                    .focused($focusedField, equals: .title)

                TextField("Amount", text: $amount)
                    .focused($focusedField, equals: .amount)
                    .keyboardType(.decimalPad)

                TextEditor(text: $notes)
                    .focused($focusedField, equals: .notes)
                    .frame(height: 100)
            }
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                // Show number shortcuts only for amount field
                if focusedField == .amount {
                    HStack(spacing: 12) {
                        ForEach(["10", "25", "50", "100"], id: \.self) { value in
                            Button(value) {
                                amount = value
                            }
                            .buttonStyle(.bordered)
                        }
                    }
                }

                Spacer()

                Button("Done") {
                    focusedField = nil
                }
            }
        }
    }
}

In the run, focusing Title showed only Done. Focusing Amount brought up the decimal pad with 10, 25, 50 and 100 in one capsule on the left, and tapping 25 filled the field. The .bordered style on each button didn't draw separate bordered pills: they rendered as tinted text inside the shared capsule.

Amount shortcuts above the decimal pad

Add Previous/Next buttons to move between text fields:

Swift
struct NavigableFields: View {
    @State private var field1 = ""
    @State private var field2 = ""
    @State private var field3 = ""
    @FocusState private var focusedField: Field?

    enum Field: Int, CaseIterable {
        case field1, field2, field3
    }

    var body: some View {
        Form {
            TextField("First Name", text: $field1)
                .focused($focusedField, equals: .field1)

            TextField("Last Name", text: $field2)
                .focused($focusedField, equals: .field2)

            TextField("Email", text: $field3)
                .focused($focusedField, equals: .field3)
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                Button {
                    moveToPrevious()
                } label: {
                    Image(systemName: "chevron.up")
                }
                .disabled(!hasPrevious)

                Button {
                    moveToNext()
                } label: {
                    Image(systemName: "chevron.down")
                }
                .disabled(!hasNext)

                Spacer()

                Button("Done") {
                    focusedField = nil
                }
            }
        }
    }

    var hasPrevious: Bool {
        guard let current = focusedField else { return false }
        return current.rawValue > 0
    }

    var hasNext: Bool {
        guard let current = focusedField else { return false }
        return current.rawValue < Field.allCases.count - 1
    }

    func moveToPrevious() {
        guard let current = focusedField,
              current.rawValue > 0,
              let previous = Field(rawValue: current.rawValue - 1) else { return }
        focusedField = previous
    }

    func moveToNext() {
        guard let current = focusedField,
              current.rawValue < Field.allCases.count - 1,
              let next = Field(rawValue: current.rawValue + 1) else { return }
        focusedField = next
    }
}

With Email focused, the down chevron was disabled and the up chevron active, and both chevrons sat together in one capsule.

Previous and next chevrons above the keyboard with the last field focused

Styled Toolbar Buttons

Customize the appearance of toolbar buttons:

Swift
struct StyledToolbar: View {
    @State private var text = ""
    @FocusState private var isFocused: Bool

    var body: some View {
        TextEditor(text: $text)
            .focused($isFocused)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Button {
                        // Action
                    } label: {
                        Label("Bold", systemImage: "bold")
                            .labelStyle(.iconOnly)
                    }
                    .buttonStyle(.bordered)
                    .tint(.blue)

                    Button {
                        // Action
                    } label: {
                        Label("Italic", systemImage: "italic")
                            .labelStyle(.iconOnly)
                    }
                    .buttonStyle(.bordered)
                    .tint(.blue)

                    Spacer()

                    Button("Done") {
                        isFocused = false
                    }
                    .buttonStyle(.borderedProminent)
                }
            }
            .padding()
    }
}

Clear Text Button

Add a button to quickly clear the text field:

Swift
struct ClearableTextField: View {
    @State private var searchText = ""
    @FocusState private var isFocused: Bool

    var body: some View {
        TextField("Search", text: $searchText)
            .focused($isFocused)
            .textFieldStyle(.roundedBorder)
            .padding()
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Button("Clear") {
                        searchText = ""
                    }
                    .disabled(searchText.isEmpty)

                    Spacer()

                    Button("Done") {
                        isFocused = false
                    }
                }
            }
    }
}

Counter Display

Show character count or word count:

Swift
struct CounterToolbar: View {
    @State private var text = ""
    @FocusState private var isFocused: Bool

    let maxCharacters = 280

    var remainingCharacters: Int {
        maxCharacters - text.count
    }

    var counterColor: Color {
        switch remainingCharacters {
        case ..<0: return .red
        case 0..<20: return .orange
        default: return .secondary
        }
    }

    var body: some View {
        VStack {
            TextEditor(text: $text)
                .focused($isFocused)
                .border(Color.gray.opacity(0.3), width: 1)
                .padding()

            Spacer()
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                Text("\(remainingCharacters)")
                    .foregroundColor(counterColor)
                    .font(.caption)
                    .monospacedDigit()

                Spacer()

                Button("Post") {
                    submitPost()
                }
                .disabled(text.isEmpty || remainingCharacters < 0)
                .bold()
            }
        }
    }

    func submitPost() {
        // Submit the post
        isFocused = false
    }
}

Integration with TextEditor

TextEditor works seamlessly with keyboard toolbars:

Swift
struct NoteEditor: View {
    @State private var note = ""
    @FocusState private var isEditing: Bool

    var body: some View {
        NavigationView {
            TextEditor(text: $note)
                .focused($isEditing)
                .padding()
                .navigationTitle("New Note")
                .toolbar {
                    ToolbarItemGroup(placement: .keyboard) {
                        Button {
                            insertTimestamp()
                        } label: {
                            Image(systemName: "clock")
                        }

                        Spacer()

                        Button("Done") {
                            isEditing = false
                        }
                    }
                }
        }
    }

    func insertTimestamp() {
        let timestamp = Date().formatted(date: .abbreviated, time: .shortened)
        note += "\n\(timestamp): "
    }
}

Important Notes

iOS Version: The .keyboard placement requires iOS 15+. For earlier versions, you'll need UIKit input accessory views.

Focus Management: Always use @FocusState to properly manage keyboard focus. Without it, you can't programmatically dismiss the keyboard.

Toolbar Persistence: The toolbar appears only when the keyboard is visible. It automatically animates in and out with the keyboard.

Multiple Fields: When you have multiple text fields, the keyboard toolbar is shared across all focused fields unless you conditionally change its content.

Sheets: The toolbar also worked inside a NavigationStack presented as a sheet (the note editor example), with the clock and Done buttons in the same floating style.

Testing: I only tested in the simulator with the software keyboard. Check on a real device that the buttons are comfortable touch targets (Apple's guidance is at least 44x44 points).

Best Practices

Do:

  • Keep toolbar buttons concise (3-5 buttons maximum)
  • Always include a "Done" button for dismissing the keyboard
  • Use SF Symbols for icon buttons
  • Provide visual feedback (disabled states, colors)

Don't:

  • Overcrowd the toolbar with too many buttons
  • Use text labels that are too long
  • Forget to handle keyboard dismissal
  • Rely on toolbar for critical functionality (it's hidden when keyboard is dismissed)

The keyboard toolbar is a clean, native way to enhance text input in your SwiftUI apps without the complexity of UIKit bridges.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.