All articles
6 min·

safeAreaInset vs safeAreaBar in SwiftUI

safeAreaInset and safeAreaBar pin fixed content to the edge of the screen. Visual differences, traps, and how to pick the right SwiftUI modifier.

Par Carolane Lefebvre
safeAreaInset vs safeAreaBar in SwiftUI

Want to pin a button or a bar to the bottom of your screen, on top of a scroll view, without the content sliding underneath it? That is exactly what these two modifiers are for. Since iOS 26 Apple has shipped a second one, and the difference is not just cosmetic 👀.

In this article we will look at what each one does, how they behave visually, which traps to avoid, and above all which one to reach for depending on your context.


The problem in the first place

Picture a classic screen: a list that scrolls, a "Continue" button pinned at the bottom. That pattern is everywhere: checkout pages, onboarding, multi-step forms, audio players, and so on.

Before iOS 15, the answer was a hand-built ZStack. You stacked your scroll view and your button, then added a .padding(.bottom, [value]) on the content so it would not be hidden by the button.

And you can probably already see the problem: how do you pick that padding value? You had to guess the height of the bar, deal with the home indicator on iPhones without a physical button, and adjust the padding whenever the content of the bar changed: longer labels, taller buttons.

You can see where this goes: a maintenance nightmare 🥲

Since iOS 15, safeAreaInset has solved it properly, with the system working out the needed space on its own. Since iOS 26, safeAreaBar offers an alternative with a different visual rendering, one that sits closer to Apple's design system. Let's see what each gives you.

.safeAreaInset: the classic that gets the job done

This is the modifier we have been using for years, since it landed in iOS 15. You give it an edge, .bottom, .top, .leading, .trailing, and a view in a view builder. It pins your view to that edge, and the scrollable content shifts automatically to make room for it. Nothing left to calculate: the system measures your bar and adjusts the inset by itself.

ScrollView {
    LazyVStack {
        ForEach(steps) { step in
            StepRow(step)
        }
    }
}
.safeAreaInset(edge: .bottom) {
    Button("Continue") { /* ... */ }
        .buttonStyle(.borderedProminent)
        .padding()
}

In practice the button sits at the bottom with its own opaque background. The content scrolls and stops just above it. It is clean, it is solid, and above all it is predictable.

💡 Worth knowing:

  • It works with any scrollable content (ScrollView, List, Form, TextEditor).

  • The default alignment is .center, but the alignment parameter lets you push your bar to the left (.leading) or to the right (.trailing).

Example: an audio player

ScrollView {
    LazyVStack {
        ForEach(songs) { song in
            SongRow(song)
        }
    }
}
.safeAreaInset(edge: .bottom) {
    MiniPlayerView(currentSong: nowPlaying)
        .background(.regularMaterial)
}

.safeAreaBar: the new one from iOS 26

Same shape as .safeAreaInset: an edge, a view builder. So where is the twist?

Here, the difference plays out entirely in the visual rendering, and that is where it gets interesting.

ScrollView {
    LazyVStack {
        ForEach(steps) { step in
            StepRow(step)
        }
    }
}
.safeAreaBar(edge: .bottom) {
    Button("Continue") { /* ... */ }
        .buttonStyle(.borderedProminent)
        .padding()
}

With safeAreaBar, the bar turns translucent. Content scrolling underneath does not vanish abruptly, it fades progressively into the bar, exactly the way system toolbars and tab bars have behaved since iOS 26. It is the same fade you see in apps like Messages, Safari or Mail when you scroll down.

Naturally, having shipped with iOS 26, safeAreaBar plays well with Liquid Glass. If your app adopts the new iOS 26 design, your custom bars behave like the native ones: translucent, fluid, part of the system chrome. Visually, the user can no longer tell your custom bar from a native one.

💡 A few subtleties:

  • Content stays visible under the bar. That is the point of translucency. But it also means that if you put something important in the last row of your list, it will be partly covered by the bar. Test it 😉.

  • safeAreaBar respects scrollEdgeEffectStyle. If you customize the scroll edge effect in your view (say .scrollEdgeEffectStyle(.soft, for: .bottom)), the bar follows that style. safeAreaInset ignores the modifier entirely.

  • It works on .top too. A translucent header blending into the scroll is a perfectly valid use case (look at Mail on iOS 26).


So which one?

You have to support iOS 18 or older?

Use safeAreaInset, since safeAreaBar only exists from iOS 26. Call it without an #available check and your app will fall over on earlier versions.

Your bar has to be fully opaque and separate from the content?

safeAreaInset, even if you target iOS 26. It is the right call for an action button with a coloured background, a price bar in a cart, a mini player, or anything that has to stand visually apart from the rest. The translucency of safeAreaBar would work against you here: you want the user to read the bar as a distinct element.

You are on iOS 26 and your bar should blend into the system look?

safeAreaBar. It is the natural choice if you adopt Liquid Glass and want your custom bars to melt into the OS chrome. It is also the right one when your bar holds secondary navigation controls that look like toolbars: the fade makes them feel more natural.

You want to support both?

You can lean on a ViewModifier with an availability check that switches for you:

struct AdaptiveBottomBar<Bar: View>: ViewModifier {
    @ViewBuilder var bar: Bar

    func body(content: Content) -> some View {
        if #available(iOS 26, *) {
            content.safeAreaBar(edge: .bottom) { bar }
        } else {
            content.safeAreaInset(edge: .bottom) { bar }
        }
    }
}

extension View {
    func adaptiveBottomBar<Bar: View>(@ViewBuilder bar: () -> Bar) -> some View {
        modifier(AdaptiveBottomBar(bar: bar()))
    }
}

Which you then use in your views:

ScrollView {
    // your content
}
.adaptiveBottomBar {
    Button("Continue") { /* ... */ }
        .buttonStyle(.borderedProminent)
        .padding()
}

safeAreaBar on iOS 26+, safeAreaInset as the fallback. Same API in your code, the right rendering on each version, zero #available to juggle in your screens.

The takeaway

safeAreaInset is not deprecated and is not going anywhere. It remains the right choice for backwards compatibility, and for bars that have to stay opaque and visually separated from the content. It is the default modifier as long as you support iOS 25 or older.

safeAreaBar is what iOS 26 expects: translucent, integrated, Liquid Glass friendly. If your deployment target is iOS 26 and your bar belongs to the app chrome rather than floating above the content, that is the one.

Same API shape, a completely different visual philosophy. Do not pick by default, pick based on what you want the user to perceive.

Hope this helps, have fun coding!

App mentioned

Keepio

Capture et organise tes idées

Commentaires

Connecte-toi pour laisser un commentaire.

Aucun commentaire pour le moment. Sois le premier !

More articles

.task vs .onAppear vs .refreshable en SwiftUI

iOS 26 a livré 5 améliorations discrètes en SwiftUI

5 modifiers SwiftUI pour maitriser vos Sheets