NSToolbar on macOS: Customization, Validation, and Persistent Identity
Build adaptable NSToolbar interfaces with stable item identifiers, delegate-backed customization, command validation, autosave, and overflow-safe behavior.
An NSToolbar is a user-configurable command surface attached to a window, not a row of fixed buttons that the application can assume will always be present. Users can rearrange, remove, or customize items when the toolbar allows it. A toolbar can also display fewer items than its default configuration when the window is narrow. Production code should define stable item identifiers, return items through a delegate, validate command state from the model, and treat saved customization as user-owned state.
The toolbar identifier names a toolbar configuration. Reuse the same identifier when windows should share the same toolbar configuration; use different identifiers when the available commands or semantics differ. An identifier is a persistence key, so changing it can make the application behave as if the user has never customized that toolbar.
Define identifiers and defaults
When creating a toolbar programmatically, implement toolbarDefaultItemIdentifiers(_:); AppKit uses that list when no saved configuration exists and to populate the customization palette. Implement the allowed identifiers separately, because those describe what users may add. Special identifiers such as flexible space, separator, or space can also appear in those arrays. Return items in the order that makes sense for a fresh install.
import AppKit
final class DocumentToolbarDelegate: NSObject, NSToolbarDelegate {
static let refreshID = NSToolbarItem.Identifier("document.refresh")
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[Self.refreshID, .flexibleSpace, .print]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[Self.refreshID, .print, .flexibleSpace, .space, .separator]
}
func toolbar(_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
guard itemIdentifier == Self.refreshID else { return nil }
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.label = "Refresh"
item.paletteLabel = "Refresh Document"
item.target = self
item.action = #selector(refresh(_:))
return item
}
@objc private func refresh(_ sender: Any?) {
// Invoke the document's refresh command through its command owner.
}
}
The delegate object must be retained by an owner because NSToolbar.delegate is weak. The sample shows a custom item and built-in print identifier; a real app should also connect the action to the correct document or window controller. Do not use an index as item identity. Item positions change when users customize, items overflow, or a toolbar is rebuilt.
Customization is a user preference
Set allowsUserCustomization only when the commands can remain understandable in different orders and when optional items are safe to remove. autosavesConfiguration lets AppKit persist the toolbar configuration under its identifier. If enabled, do not overwrite the item’s configuration on every window construction by assigning a fresh itemIdentifiers array; Apple warns that doing so can override user customizations.
If the product must remove a deprecated command, handle existing saved configurations deliberately. Keep old identifiers resolvable when they can be safely mapped, or migrate the configuration with an explicit versioned policy. Returning nil for an item identifier can leave a saved toolbar item that no longer renders. Test clean preferences and real previously customized preferences.
The customization palette depends on your allowed item identifiers and item construction method. Provide human-readable label and paletteLabel values, appropriate images, and tooltips where necessary. Toolbar content can be displayed as icon, text, or both; do not assume the icon alone communicates a unique command. Validate accessibility labels and keyboard focus after changing the display mode.
Toolbar items may be created for the customization palette before they are visible in a window. The factory should be safe to call repeatedly and must not make assumptions about the current key window. Keep construction cheap, avoid starting network or document work while constructing an item, and attach its command target only when that target has a stable owner. The willBeInsertedIntoToolbar argument lets an implementation distinguish insertion behavior when it genuinely needs to, but it should not become an alternate source of truth for whether a command is allowed.
Validate commands from current document state
Toolbar item validation should reflect the command’s actual availability. Use the responder chain or NSToolbarItemValidation to enable or disable a command based on selection, document state, permissions, and current work. Do not leave a toolbar button enabled because an earlier selection allowed the action. A stale toolbar state can let a user issue an invalid operation from a view that has since changed.
Validation must be cheap and side-effect free. It may occur during window update cycles and should not fetch from the network, mutate the document, or synchronously scan a large directory. Cache derived readiness in the model, update it when state changes, and let validation read that state. If an operation becomes unavailable after the user clicks, validate again at execution time; button state alone is not authorization or a transaction lock.
For commands shared with menus and keyboard shortcuts, keep one command implementation and one state rule. A toolbar-specific handler should adapt the invocation but not duplicate business logic. This keeps keyboard command validation and toolbar validation aligned and prevents the same command from succeeding through one affordance while failing through another.
When a command is disabled, make the reason discoverable through a tooltip, status text, or nearby contextual explanation where appropriate. A disabled icon can look like a rendering defect if its state has no explanation. Conversely, do not leave an unavailable command enabled merely to teach users that it fails after click; validation should guide them, while execution still checks the invariant.
Responsive behavior and overflow
Toolbars adapt to window width and item priorities. Items can move to an overflow menu when there is insufficient space. Treat an item appearing in overflow as a valid interaction path, not a rendering error. Actions should not depend on the toolbar item’s view being visible or on a coordinate relative to its original location.
Choose which controls belong in the toolbar based on frequency and discoverability. A rare destructive action may belong in a menu instead. A search field or segmented control has different sizing behavior than a compact action item. Test narrow and wide windows, full screen, titlebar integration, different display modes, localization, and high contrast. Verify that labels do not cause important commands to disappear without a discoverable equivalent.
Apple’s current docs note that allowsDisplayModeCustomization has a default that differs for apps linked on a recent macOS SDK compared with older-linked apps. Avoid relying on that default as product policy. Set the property based on whether users benefit from changing display mode, and check availability for the deployment target before using newer APIs.
Multiple windows and toolbar state
If several document windows share one toolbar identifier, their customization can be shared. This is useful when command meaning and item set are consistent. If one window type has additional commands or a different interaction model, use a separate toolbar identifier or a deliberate common allowed set. Avoid mutating a shared delegate’s current-document pointer without tying it to the window whose toolbar is being validated.
Toolbar ownership should follow window ownership. A window controller can create the toolbar, retain its delegate, and route commands to its document. A global singleton delegate is usually a poor owner for document-specific commands because two windows can make it ambiguous which document receives an action. Test activating a second window while the first still exists.
Appearance, accessibility, and state
Use symbols that remain distinguishable at toolbar scale and offer textual display when meaning is not obvious. Provide accessibility labels for icon-only items, and make selected state visible for toggles or segmented modes. Do not imply that a command is unavailable solely through low-contrast color. When the state changes, update validation and visual feedback from the shared model.
Keep transient progress separate from command availability. A long-running command may change into a stop or cancel action, become disabled, or remain available for another independent document. Decide which behavior matches the domain. If an operation can be started twice accidentally, use an operation token or idempotent execution rule rather than relying on disabling the item quickly enough.
Acceptance matrix
Test a first-run toolbar, user customization, removed and reordered items, saved configuration after app upgrade, two windows with different documents, overflow, full screen, titlebar integration, disabled command states, rapid model changes, and accessibility navigation. Assert every allowed identifier has a valid item factory, every custom action reaches the correct owner, and a stale configuration does not silently lose all commands.
Log toolbar identifier, item identifier, command availability reason, and action result without exposing document content. When diagnosing a missing item, capture whether the identifier is default, allowed, saved in a custom configuration, and currently visible or in overflow. Those are different states and require different fixes.
NSToolbar gives an app a native, customizable command surface. The app owns stable identity, safe customization, command validation, cross-window routing, and upgrade compatibility. Treat the user’s toolbar layout as durable preference data, not as a disposable view arrangement.
Related:
- NSMenuItem Validation on macOS: Enable Commands From Real State
- AppKit Window Restoration: Reopen the Right Workspace, Not Just a Window
Sources: