SwiftUI Toolbars in iOS 26: Spacers, Badges, and Scroll Edge Blur
Toolbars in SwiftUI have always been a “place items and hope for the best” API. You could specify a placement, but you had almost no control over spacing, no way to badge an item, and no built-in strategy for handling the visual transition when content scrolls behind a glass navigation bar. iOS 26 fixes all three gaps.
This post covers the four toolbar additions that shipped with iOS 26: ToolbarSpacer, badge support on toolbar items,
monochrome icon rendering, and scroll edge blur behavior. We won’t cover the broader navigation and tab bar changes —
those belong to the iOS 26 navigation migration guide.
Contents
- The Problem
- ToolbarSpacer: Precise Toolbar Layout
- Badge Support on Toolbar Items
- Monochrome Icon Rendering
- Scroll Edge Blur
- Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Consider a movie catalog app. You have a navigation bar with a handful of actions: filter, sort, and a notification bell
that should show an unread count. Before iOS 26, achieving precise spacing between those items required invisible
Spacer views wrapped in ToolbarItem, and badges were simply not possible without dropping down to UIKit.
// Pre-iOS 26: Attempting to space toolbar items and show a badge
struct MovieCatalogToolbar: View {
@State private var unreadCount = 3
var body: some View {
NavigationStack {
MovieListView()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(action: filterMovies) {
Image(systemName: "line.3.horizontal.decrease.circle")
}
}
// No way to add fixed spacing here
ToolbarItem(placement: .topBarTrailing) {
Button(action: showNotifications) {
// No native badge API -- resort to ZStack overlay
ZStack(alignment: .topTrailing) {
Image(systemName: "bell")
if unreadCount > 0 {
Text("\(unreadCount)")
.font(.caption2)
.padding(4)
.background(Color.red)
.clipShape(Circle())
.foregroundStyle(.white)
}
}
}
}
}
}
}
// Simplified for clarity
private func filterMovies() {}
private func showNotifications() {}
}
That ZStack badge is fragile. It does not respect Dynamic Type, does not animate consistently with the toolbar, and
breaks when the system applies the Liquid Glass visual treatment. iOS 26 gives you
real primitives for each of these problems.
ToolbarSpacer: Precise Toolbar Layout
ToolbarSpacer is a new view type you place directly
inside a toolbar block. It accepts a width parameter or a semantic style that tells the system how to distribute
space.
Fixed-Width Spacing
Use .fixed when you need a consistent gap between toolbar items — for example, to visually group related actions
apart from unrelated ones.
@available(iOS 26.0, *)
struct PixarMovieCatalogView: View {
var body: some View {
NavigationStack {
List(pixarMovies) { movie in
MovieRow(movie: movie)
}
.navigationTitle("Pixar Vault")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Sort", systemImage: "arrow.up.arrow.down") {
// Sort action
}
}
ToolbarSpacer(.fixed)
ToolbarItem(placement: .topBarTrailing) {
Button("Filter", systemImage: "line.3.horizontal.decrease.circle") {
// Filter action
}
}
}
}
}
}
.fixed inserts a system-defined gap — typically 8 points on iPhone and 12 points on iPad — that adapts to the
current size class. You do not provide a custom point value; the system decides what “fixed” means so your toolbar stays
consistent with platform conventions.
Flexible Spacing
When you want one item pinned to the leading edge and another to the trailing edge of the same toolbar region, use the default flexible spacer.
ToolbarSpacer() // Flexible -- expands to fill available space
This is equivalent to how Spacer() works in an HStack, but scoped to the toolbar layout engine. It respects the safe
areas and glass insets that iOS 26 applies to navigation bars.
Tip:
ToolbarSpaceris only valid inside atoolbarclosure. Placing it inside a regularHStackhas no effect — the compiler won’t complain, but the spacer will be ignored.
Badge Support on Toolbar Items
The .badge(_:) modifier now works on any
ToolbarItem. This is the same modifier you already
use on TabView tabs and List rows, extended to toolbar context.
@available(iOS 26.0, *)
struct ToyBoxDashboardView: View {
@State private var pendingRepairs = 5
var body: some View {
NavigationStack {
ToyInventoryList()
.navigationTitle("Toy Box HQ")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Repairs", systemImage: "wrench.and.screwdriver") {
// Show repair queue
}
.badge(pendingRepairs)
}
ToolbarSpacer(.fixed)
ToolbarItem(placement: .topBarTrailing) {
Button("Alerts", systemImage: "bell") {
// Show alerts
}
.badge("New")
}
}
}
}
}
Two forms are available: .badge(_ count: Int) for numeric badges and .badge(_ label: Text) (or its string literal
variant) for text badges. A count of zero hides the badge automatically.
The system renders the badge in a position consistent with the Liquid Glass design language: a small capsule anchored to the top-trailing corner of the icon. On iOS 26, badges inherit the tint from the toolbar’s color scheme and animate in and out with a spring transition.
Note: Badge rendering on toolbar items requires iOS 26. On earlier versions, the
.badgemodifier on aToolbarItemcompiles but has no visible effect. If you back-deploy, wrap it in an#availablecheck or use theZStackoverlay approach as a fallback.
Monochrome Icon Rendering
iOS 26 introduces a default monochrome rendering mode for toolbar SF Symbols. In previous releases, multicolor and hierarchical symbols in the toolbar rendered with their full palette, which could clash with the glass material behind them. Now, toolbar items automatically render icons in monochrome unless you explicitly opt out.
@available(iOS 26.0, *)
struct MonsterScareFloorView: View {
var body: some View {
NavigationStack {
ScareFloorDashboard()
.navigationTitle("Scare Floor F")
.toolbar {
// Renders monochrome by default in iOS 26
ToolbarItem(placement: .topBarTrailing) {
Button("Leaderboard", systemImage: "trophy.fill") {
// Show scare leaderboard
}
}
// Opt out to keep the original symbol colors
ToolbarItem(placement: .topBarTrailing) {
Button("Energy", systemImage: "bolt.fill") {
// Show energy levels
}
.symbolRenderingMode(.hierarchical)
}
}
}
}
}
The trophy.fill symbol renders as a single-tone icon matching the toolbar tint, while bolt.fill retains hierarchical
rendering because we explicitly override the default. This monochrome behavior is consistent with how Apple’s own apps
(Messages, Photos, Maps) render toolbar icons in iOS 26.
Apple Docs:
symbolRenderingMode(_:)— SwiftUI
If you have custom SF Symbols exported from the SF Symbols app, test them in monochrome mode. Symbols designed with multiple layers may lose visual clarity when flattened to a single color. The fix is to ensure your custom symbol’s monochrome variant has enough visual weight to remain legible at toolbar scale (typically 22x22 points).
Scroll Edge Blur
The most visually striking toolbar change in iOS 26 is scroll edge blur. When content scrolls behind a Liquid Glass navigation bar, the bar applies a progressive Gaussian blur to the content underneath. This replaces the abrupt material transition of previous releases and creates a smooth, depth-aware separation between chrome and content.
How It Works
Scroll edge blur activates automatically on any NavigationStack or NavigationSplitView that contains a scrollable
view (List, ScrollView, Form). You do not need to opt in. The system detects when content intersects the
navigation bar’s edge and ramps up the blur intensity proportionally.
Controlling the Behavior
If you need to suppress or customize the blur, use the
toolbarBackgroundVisibility(_:for:)
modifier.
@available(iOS 26.0, *)
struct PixarFilmArchiveView: View {
var body: some View {
NavigationStack {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(pixarFilms) { film in
FilmCardView(film: film)
}
}
.padding()
}
.navigationTitle("Pixar Film Archive")
// Suppress blur: bar becomes fully transparent at scroll edge
.toolbarBackgroundVisibility(.hidden, for: .navigationBar)
}
}
}
Three strategies are available:
| Modifier | Behavior |
|---|---|
| Default (no modifier) | Progressive blur as content scrolls under the bar |
.toolbarBackgroundVisibility(.visible, ...) | Opaque glass bar at all times — no blur transition |
.toolbarBackgroundVisibility(.hidden, ...) | Fully transparent bar — content flows behind, no blur |
Warning: Hiding the toolbar background with
.toolbarBackgroundVisibility(.hidden)also removes the glass material entirely. Your navigation title and toolbar items will render directly over the scrolling content with no backdrop. Ensure sufficient contrast, or pair it with.toolbarForegroundStyle(.primary)to force a readable text color.
Advanced Usage
Combining Badges, Spacers, and Bottom Bars
Toolbar items are not limited to the navigation bar. In iOS 26, .bottomBar placement also supports badges and spacers,
which is useful for editing toolbars and document-style interfaces.
@available(iOS 26.0, *)
struct PixarStoryboardEditor: View {
@State private var selectedSceneCount = 0
@State private var unsavedChanges = true
var body: some View {
NavigationStack {
StoryboardCanvas()
.navigationTitle("Storyboard Editor")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button("Scenes", systemImage: "rectangle.on.rectangle") {
// Manage scenes
}
.badge(selectedSceneCount)
}
ToolbarSpacer(.fixed)
ToolbarItem(placement: .bottomBar) {
Button("Save", systemImage: "square.and.arrow.down") {
// Save storyboard
}
.badge(unsavedChanges ? "!" : nil)
}
}
}
}
}
Animating Badge Changes
Badge transitions are automatic in iOS 26. When a badge count changes, the system animates the update with a spring
transition. However, if you are updating the badge inside an explicit withAnimation block, the badge animation will
merge with your transaction, which can produce unexpected timing.
// Let the system handle badge animation
pendingRepairs -= 1 // Badge animates automatically
// If wrapped in withAnimation, the badge inherits that curve
withAnimation(.easeInOut(duration: 0.5)) {
pendingRepairs -= 1 // Badge uses your 0.5s ease-in-out
}
Keep badge updates outside explicit animation blocks unless you intentionally want to synchronize them with other view transitions.
Scroll Edge Blur with Custom Toolbars
Scroll edge blur respects custom toolbar content created with ToolbarItem(placement: .principal). If you replace the
navigation title with a custom view (say, a segmented picker), the blur still activates based on the scroll offset —
your custom content simply sits on top of the blurred glass.
@available(iOS 26.0, *)
struct WallEArchiveView: View {
@State private var category = "Directive"
var body: some View {
NavigationStack {
DirectiveListView(category: category)
.toolbar {
ToolbarItem(placement: .principal) {
Picker("Category", selection: $category) {
Text("Directive").tag("Directive")
Text("Plant Log").tag("Plant Log")
}
.pickerStyle(.segmented)
.frame(maxWidth: 240)
}
}
}
}
}
The segmented picker renders on glass with the blur activating beneath it as the list scrolls. This is a clean pattern for category-based filtering without a dedicated header view.
Performance Considerations
Toolbar badges and spacers are lightweight. They are rendered by the system chrome layer, not your view hierarchy, so they add negligible overhead to your SwiftUI render passes.
Scroll edge blur, however, involves real-time Gaussian blur computed on the GPU. On modern devices (A15 and later), this is effectively free. On older hardware that happens to run iOS 26 (if applicable), the system gracefully degrades: the blur radius may be reduced, or the bar may fall back to a static material. You do not need to profile or tune this — the system handles degradation automatically.
One thing to watch: if you stack multiple glass-blurred regions (for example, a navigation bar with scroll edge blur, a
bottom toolbar with its own glass material, and a floating action button with .glassEffect), each blur pass compounds.
In Instruments, look for the CABackdropLayer cost in the Core Animation profiler. On supported hardware this stays
under 2ms per frame, but it is worth verifying on your minimum deployment target.
Apple Docs:
toolbar(content:)— SwiftUI
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Grouping related toolbar actions | Use ToolbarSpacer(.fixed) between action clusters |
| Showing unread counts or indicators | Use .badge(_:) — handles Dynamic Type and glass |
| Pinning items to opposite edges | Use ToolbarSpacer() (flexible) between them |
| Custom icon colors in the toolbar | Override with .symbolRenderingMode(.hierarchical) |
| Full-bleed hero images behind nav bar | Use .toolbarBackgroundVisibility(.hidden) |
| Static content that does not scroll | Scroll edge blur is irrelevant; no action needed |
| Back-deploying to iOS 18 or earlier | Wrap in if #available(iOS 26.0, *) |
Summary
ToolbarSpacer(.fixed)andToolbarSpacer()give you real layout control inside toolbar closures — no more invisible buttons or padding hacks..badge(_:)onToolbarItemis a first-class API that respects Dynamic Type, animates automatically, and integrates with the Liquid Glass material.- Monochrome icon rendering is the new default for toolbar SF Symbols in iOS 26. Override with
.symbolRenderingMode(_:)when you need color. - Scroll edge blur activates automatically on scrollable views and can be controlled with
.toolbarBackgroundVisibility(_:for:).
These four additions are small individually, but together they eliminate the most common toolbar workarounds that have accumulated since SwiftUI 1.0. For the broader navigation story — tab bars, sidebars, and split views — see the iOS 26 navigation migration guide.