Toast Banner
A toast modifier that slides a banner from the top, announces it to VoiceOver and dismisses by itself.

- Feedback
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- toast · banner · notification · alert · message · auto dismiss · feedback · toast · aviso · notificación · alerta · mensaje · banner · autodescartar
Description
Section titled “Description”Slides a banner in from the top, announces it to VoiceOver and dismisses it after duration seconds.
With Reduce Motion on, the banner fades instead of sliding.
The capsule shown by the .toast(isPresented:) modifier.
struct ToastBannerDemo: View { @State private var showsToast = false
var body: some View { ZStack { LinearGradient(colors: [.mint.opacity(0.35), .teal.opacity(0.25)], startPoint: .top, endPoint: .bottom) .ignoresSafeArea()
VStack(spacing: 28) { Image(systemName: "doc.text.fill") .font(.largeTitle) .imageScale(.large) .foregroundStyle(.teal.gradient) .accessibilityHidden(true)
Text("Quarterly notes") .font(.title2.bold())
Button { showsToast = true } label: { Text("Save") .font(.headline) .foregroundStyle(.white) .padding(.horizontal, 48) .padding(.vertical, 16) .background(.teal.gradient, in: .capsule) } .buttonStyle(.plain) } .frame(maxWidth: .infinity, maxHeight: .infinity) } .toast(isPresented: $showsToast, message: "Changes saved") }}API reference
Section titled “API reference”toast(isPresented:message:systemImage:tint:duration:)
| Parameter | Type | Default |
|---|---|---|
isPresented | Binding<Bool> | Required |
message | LocalizedStringResource | Required |
systemImage | String | "checkmark.circle.fill" |
tint | Color | .green |
duration | Double | 2.2 |
ToastBanner
| Parameter | Type | Default |
|---|---|---|
message | LocalizedStringResource | Required |
systemImage | String | "checkmark.circle.fill" |
tint | Color | .green |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// The capsule shown by the `.toast(isPresented:)` modifier.struct ToastBanner: View { let message: LocalizedStringResource var systemImage: String = "checkmark.circle.fill" var tint: Color = .green
var body: some View { HStack(spacing: 10) { Image(systemName: systemImage) .font(.title3) .foregroundStyle(tint.gradient) .accessibilityHidden(true)
Text(message) .font(.subheadline.weight(.semibold)) } .padding(.horizontal, 20) .padding(.vertical, 14) .background(.regularMaterial, in: .capsule) .shadow(color: .black.opacity(0.15), radius: 14, y: 6) .accessibilityElement(children: .combine) }}
private struct ToastBannerModifier: ViewModifier { @Binding var isPresented: Bool let message: LocalizedStringResource let systemImage: String let tint: Color let duration: Double
@Environment(\.accessibilityReduceMotion) private var reduceMotion
func body(content: Content) -> some View { content .overlay(alignment: .top) { if isPresented { ToastBanner(message: message, systemImage: systemImage, tint: tint) .padding(.top, 8) .transition(reduceMotion ? .opacity : .move(edge: .top).combined(with: .opacity)) .task(id: message) { await announceAndDismiss() } } } .animation(reduceMotion ? .easeInOut(duration: 0.2) : .spring(duration: 0.45, bounce: 0.3), value: isPresented) }
private func announceAndDismiss() async { AccessibilityNotification.Announcement(String(localized: message)).post() do { try await Task.sleep(for: .seconds(duration)) } catch { return } isPresented = false }}
extension View { /// Slides a banner in from the top, announces it to VoiceOver and dismisses it after `duration` seconds. /// /// With Reduce Motion on, the banner fades instead of sliding. func toast( isPresented: Binding<Bool>, message: LocalizedStringResource, systemImage: String = "checkmark.circle.fill", tint: Color = .green, duration: Double = 2.2 ) -> some View { modifier(ToastBannerModifier( isPresented: isPresented, message: message, systemImage: systemImage, tint: tint, duration: duration )) }}
// MARK: - Demo
struct ToastBannerDemo: View { @State private var showsToast = false
var body: some View { ZStack { LinearGradient(colors: [.mint.opacity(0.35), .teal.opacity(0.25)], startPoint: .top, endPoint: .bottom) .ignoresSafeArea()
VStack(spacing: 28) { Image(systemName: "doc.text.fill") .font(.largeTitle) .imageScale(.large) .foregroundStyle(.teal.gradient) .accessibilityHidden(true)
Text("Quarterly notes") .font(.title2.bold())
Button { showsToast = true } label: { Text("Save") .font(.headline) .foregroundStyle(.white) .padding(.horizontal, 48) .padding(.vertical, 16) .background(.teal.gradient, in: .capsule) } .buttonStyle(.plain) } .frame(maxWidth: .infinity, maxHeight: .infinity) } .toast(isPresented: $showsToast, message: "Changes saved") }}
#Preview { ToastBannerDemo()}