A lightweight, fully-customizable toast / notification library for iOS, built with SwiftUI and iOS 26's Liquid Glass.
Show a compact message — icon + title + message + action button — above any window, from anywhere in your app (SwiftUI, view models, or UIKit). Ships with semantic presets and deep customization.
- 🎯 Present from anywhere — floats above every window on its own passthrough
UIWindow; touches outside the toast pass straight through to your app. - 🧩 Semantic presets —
.success,.error,.info,.warning, plus fully custom. - 🎨 Deep customization — background, corner radius, fonts, colors, icon, shadow, padding, max width.
- 🪟 iOS 26 Liquid Glass — opt-in glass for the action button and/or the whole toast, each with an optional tint.
- 👆 Four ways to dismiss — auto after a duration, swipe, tap, or the action button (with a callback). The auto-dismiss timer pauses while you're dragging.
- 🔀 Sequential queue — multiple toasts show one at a time, in order.
- 🧵 Three APIs — global
Toastfacade, a SwiftUI.toast(…)modifier, and aUIViewControllerbridge. - ♿️ Accessible — Dynamic Type, VoiceOver announcements, Reduce Motion fallback, 44 pt tap targets.
- 🌓 Adaptive — soft spring in / smooth glide out, light & dark aware defaults.
| iOS | 26.0+ |
| Swift | 6.0+ (Swift 6 language mode) |
| Xcode | 26+ |
File ▸ Add Package Dependencies… and enter:
https://github.com/kushal211/ToastMessage.git
dependencies: [
.package(url: "https://github.com/kushal211/ToastMessage.git", from: "1.0.0")
],
targets: [
.target(name: "YourApp", dependencies: ["ToastMessage"])
]Then import ToastMessage.
import ToastMessage
// Semantic preset with an action + callback
Toast.success("Kushal's Request has been completed",
action: .init("Undo") { viewModel.undo() })
Toast.error("Something went wrong", message: "Please try again")
Toast.info("Heads up", message: "You have 3 new messages")
Toast.warning("Low storage", message: "Less than 1 GB remaining")That's it — no setup, no view to install. The library manages its own window.
![]() |
Toast.success(_:message:action:) |
![]() |
Toast.error(_:message:action:) |
![]() |
Toast.info(_:message:action:) |
![]() |
Toast.warning(_:message:action:) |
Each preset ships a distinct deep-tinted background (green / red / blue / amber) and matching icon, so they're recognizable at a glance. Every preset accepts optional style: and configuration: overrides.
Opt any toast into Liquid Glass — the button, the whole background, or both — each with an optional tint.
Glass button
Toast.success("Uploaded",
action: .init("Undo") { … },
style: .custom {
$0.buttonUsesGlass = true // Liquid Glass button
$0.buttonTint = .blue // optional; nil = plain glass
})Glass background
Toast.show(
content: ToastContent(icon: .systemImage("sparkles", tint: .yellow),
title: "Liquid Glass",
message: "The whole surface is glass with a tint.",
action: .init("Undo") { … }),
style: .custom {
$0.backgroundUsesGlass = true // whole toast = glass
$0.backgroundTint = .blue // optional tint
$0.buttonUsesGlass = true // (independent of the background)
$0.shadow = .none // glass has its own depth
})Style is split into three orthogonal value types, so you only set what you want to change.
| Field | Type | Notes |
|---|---|---|
icon |
ToastIcon? |
.systemImage("bell", tint:), .image(Image, tint:), or .none |
title |
String? |
Primary line |
message |
String? |
Secondary line |
action |
ToastAction? |
Trailing button |
| Field | Default |
|---|---|
backgroundColor |
near-black |
backgroundUsesGlass / backgroundTint |
false / nil |
cornerRadius |
32 |
iconColor |
nil (uses the icon's own tint) |
titleFont / titleColor |
17 semibold / white |
messageFont / messageColor |
14 regular / white 70% |
buttonFont / buttonForegroundColor / buttonBackgroundColor |
16 semibold / white / white 14% |
buttonUsesGlass / buttonTint |
false / nil |
shadow |
soft drop shadow (.default, .none, or custom) |
contentPadding |
roomy for text; auto-tightened next to a button |
maxWidth |
nil (hugs content) |
Build one with the .custom { … } builder or a preset, or the memberwise ToastStyle(…) initializer.
| Field | Default |
|---|---|
edge |
.top (or .bottom) |
duration |
.seconds(3) (or .indefinite) |
dismissOnTap |
true |
dismissOnSwipe |
true |
hapticFeedback |
nil (.success / .warning / .error / .light / .medium / .heavy) |
Toast.show(
content: ToastContent(
icon: .systemImage("sparkles", tint: .yellow),
title: "Fully Custom Toast",
message: "Every ToastStyle field is set here.",
action: ToastAction("Undo", dismissesToast: true) { viewModel.undo() }
),
style: .custom { s in
s.backgroundColor = Color(red: 0.12, green: 0.10, blue: 0.24)
s.backgroundUsesGlass = false // set true for a glass surface
s.backgroundTint = nil // used only when backgroundUsesGlass
s.cornerRadius = 22
s.iconColor = .yellow // overrides the icon's own tint
s.titleFont = .system(size: 18, weight: .bold, design: .rounded)
s.titleColor = .white
s.messageFont = .system(size: 13, weight: .regular)
s.messageColor = .white.opacity(0.65)
s.buttonFont = .system(size: 15, weight: .semibold)
s.buttonForegroundColor = .white
s.buttonBackgroundColor = .white.opacity(0.18) // used when buttonUsesGlass is false
s.buttonUsesGlass = false // set true for a glass button
s.buttonTint = .purple // used only when buttonUsesGlass
s.shadow = ToastShadow(color: .black.opacity(0.45), radius: 22, x: 0, y: 12)
s.contentPadding = EdgeInsets(top: 14, leading: 20, bottom: 14, trailing: 16)
s.maxWidth = 380 // nil = hug content
},
configuration: ToastConfiguration(
edge: .bottom,
duration: .seconds(6),
dismissOnTap: true,
dismissOnSwipe: true,
hapticFeedback: .success
)
)The four preset styles are overridable — reassign them once (e.g. at app launch) and every Toast.success / .error / .info / .warning call across your app picks up your theme:
ToastStyle.success = .custom {
$0.backgroundColor = .green
$0.iconColor = .white
$0.cornerRadius = 16
$0.buttonUsesGlass = true
}
// Later, anywhere:
Toast.success("Saved") // now uses your themed success styleSet these before presenting toasts and avoid mutating them concurrently — they're lightweight global appearance defaults.
Tip: define a reusable style once and reference it everywhere:
extension ToastStyle {
static let brand = ToastStyle.custom { $0.backgroundColor = .indigo; $0.buttonUsesGlass = true }
}
Toast.show(content: ToastContent(title: "Saved"), style: .brand)// Message only — no icon, no title
Toast.show(content: ToastContent(message: "Copied to clipboard"))
// A custom SwiftUI Image as the icon
Toast.show(content: ToastContent(
icon: .image(Image("Logo"), tint: .pink),
title: "You earned a reward!"))
// Colored background + capped width (long text wraps early)
Toast.show(
content: ToastContent(icon: .systemImage("wifi.slash"),
title: "You're offline",
message: "We'll retry automatically when the connection returns."),
style: .custom { $0.backgroundColor = Color(white: 0.2); $0.iconColor = .white; $0.maxWidth = 320 })
// Persistent, action-only (won't auto-dismiss, no tap/swipe to dismiss)
Toast.warning("Delete this file?",
action: .init("Delete") { store.delete() },
configuration: .init(duration: .indefinite, dismissOnTap: false, dismissOnSwipe: false))
// Keep the toast up after the action runs
Toast.info("Syncing…",
action: .init("Details", dismissesToast: false) { showDetails() },
configuration: .init(duration: .indefinite))The same content can be presented three ways — all route through the same window/queue.
// 1. Global facade — from anywhere, incl. view models & non-view code
Toast.show(content: content, style: .brand, configuration: .init(edge: .bottom))
// 2. SwiftUI modifiers — state-driven
someView.toast(isPresented: $flag) { ToastContent(title: "Done") }
someView.toast(item: $toastContent) // fires when the binding becomes non-nil
// 3. UIKit
viewController.showToast(content, style: .brand)
viewController.showSuccessToast("Saved") // + error/info/warning- Auto: disappears after
configuration.duration(paused while a finger is down);.indefinitenever auto-dismisses. - Swipe / tap: enabled via
dismissOnSwipe/dismissOnTap. - Action button: runs its handler, then dismisses (unless
dismissesToast: false). - Programmatic:
Toast.dismiss()(current) orToast.dismissAll()(current + queued). - Queue: toasts shown in quick succession appear one at a time, in order.
- Title, message, and button scale with Dynamic Type.
- A new toast posts a VoiceOver announcement; the action button is a proper accessibility action.
- Reduce Motion replaces the slide + spring with a gentle cross-fade.
- The action button meets the 44 pt minimum tap target.
The Example/ app is a gallery that exercises every feature — presets, positions, durations, Liquid Glass, a fully custom toast, and both SwiftUI modifiers. Open Example/ToastMessage.xcodeproj and run.
Contributions are welcome — bug reports, feature requests, and pull requests all help. Please read CONTRIBUTING.md before opening a PR for the local build/test workflow, coding conventions, and how changes are reviewed.
Quick version:
git clone https://github.com/kushal211/ToastMessage.git
cd ToastMessage
swift build # build the package
swift test # run the test suiteFound a bug or have an idea? Open an issue.
Kushal Panchal
- GitHub: @kushal211
- X (Twitter): @kushal211
- Portfolio: portray.work/kushal
- Company: IndiaNIC Infotech Ltd
ToastMessage is available under the MIT license. See LICENSE for details.
Copyright © 2026 Kushal Panchal.






