Skip to content

Toast Banner

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

Toast Banner, playing
Download .swift
Origin
Original work
Found by
toast · banner · notification · alert · message · auto dismiss · feedback · toast · aviso · notificación · alerta · mensaje · banner · autodescartar

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")
}
}

toast(isPresented:message:systemImage:tint:duration:)

ParameterTypeDefault
isPresentedBinding<Bool>Required
messageLocalizedStringResourceRequired
systemImageString"checkmark.circle.fill"
tintColor.green
durationDouble2.2

ToastBanner

ParameterTypeDefault
messageLocalizedStringResourceRequired
systemImageString"checkmark.circle.fill"
tintColor.green

Copy this code into your project, or download the file.

ToastBanner.swift
// 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()
}