Premium, natively-rendered toasts for Flutter — drawn above your app in
SwiftUI on iOS and Jetpack Compose on Android, with a springy slide-in
entrance, per-position vertical stacking, and async loading toasts. One
platform-neutral Dart API; no BuildContext required.
stacking.mp4Stacking — staggered in and out |
variable-color.mp4Animated icons — SF Symbol effects |
progress.mp4Progress — determinate upload |
action.mp4Action button — inline Undo |
- Native rendering — toasts are drawn on a native overlay above your
Flutter UI (SwiftUI on iOS, Jetpack Compose on Android), so they float over
everything and need no
BuildContext. The whole Dart API is identical across platforms. - Adaptive surface — on iOS, real
glassEffectLiquid Glass on iOS 26+, a frosted.ultraThinMaterialfallback on iOS 17–25, and an opaque surface under Reduce Transparency. On Android, an opaque adaptive surface (dark/light) with the same hairline stroke and drop shadow. - Springy entrance — toasts slide in from the nearest edge (down from the top, up from the bottom) with a fade + scale, and fade + blur away in place.
- Loading lifecycle — wrap a
Future; show a spinner, then morph to success/error. The call returns your value / rethrows your error, so your app owns the outcome. - Vertical stacking — each position is its own list (up to 5 toasts); newest pushes the rest along, and overflow scales + fades + blurs away in place.
- Semantic styles —
success/error/warning/info, each overridable. Icons are named as SF Symbols; on Android the same names are mapped to matching Material glyphs (falling back to the semantic default for unknown names). Customizable (adaptive) icon colors on both platforms. - Animated icons — opt into an SF Symbol effect (
bounce,pulse,wiggle,rotate,breathe,variableColor, or iOS-26drawOn) per toast (iOS only; the glyph renders statically on Android). - One action button — fully rounded, color derived from a semantic role.
- Production extras — tap-to-dismiss, replace-by-key, determinate progress,
haptics, accessibility (VoiceOver + Reduce Motion), app-wide defaults, and
activeCount/activeIdsintrospection.
- iOS 17.0+ (Liquid Glass auto-activates on iOS 26+; frosted fallback below).
- Android 7.0+ (API 24). Rendered natively in Jetpack Compose; no extra setup or Gradle changes needed in a standard Flutter app.
- Flutter 3.27+.
The Dart API, wire protocol, and behavior are identical on both platforms —
positions, stacking, semantics, actions (incl. async), promise morphs,
progress, replace-by-key, tap/swipe-to-dismiss, wall-clock auto-dismiss (which
survives backgrounding), haptics, and accessibility all work the same. Only the
native surface differs:
| iOS | Android | |
|---|---|---|
| Renderer | SwiftUI overlay (same window) | Jetpack Compose overlay (decor view) |
| Surface | Liquid Glass (26+) / .ultraThinMaterial (17–25) / opaque |
Opaque adaptive surface (dark/light) |
| Icons | SF Symbols + animated symbol effects | SF Symbol names mapped to Material glyphs (static) |
| Dynamic Island origin | Yes (top-center) | — (slide-in) |
| Min version | iOS 17.0 | Android 7.0 (API 24) |
Android renders an opaque surface rather than a blur/glass material — same hairline stroke, drop shadow, spring entrance, and fade-out — so a toast reads as a raised card that never becomes unreadable over busy content.
dependencies:
liquid_toasts: ^0.5.0import 'package:liquid_toasts/liquid_toasts.dart';
// Semantic one-liners — fire and forget, no await, no BuildContext
toast.success('Saved to favorites');
toast.error('Could not connect');
toast.warning('Low storage');
toast.info('3 updates available');
toast('Plain message'); // the object is callabletoast.warning(
'Low storage',
duration: null, // persistent until tapped/dismissed
action: ToastAction(
label: 'Manage',
role: ToastActionRole.primary,
onPressed: openStorageSettings,
),
);final user = await toast.promise(
api.signIn(email, password),
loading: 'Signing in…',
success: (u) => 'Welcome back, ${u.firstName}!',
error: 'Sign-in failed',
);
// `user` is your value; on failure the call rethrows so your try/catch fires.loading / success / error each take a String, a full Toast, or a
builder ((value) => … / (error) => …).
final t = toast.loading('Connecting…'); // handle returned synchronously
await Future.delayed(const Duration(seconds: 2));
t.update(loading: false, message: 'Connected', semantic: ToastSemantic.success);
t.dismiss();
final reason = await t.onDismissed; // always completesupdate patches only the fields you pass (rapid patches compose, in order);
replace(Toast(...)) swaps the content wholesale.
// Bottom toast
toast.show('Copied link', icon: 'link', position: ToastPosition.bottomCenter);
// Replace-or-update instead of stacking duplicates
toast.info('Reconnecting…', groupKey: 'net', duration: null);
// Determinate progress via patch updates
final t = toast.show('Uploading…', duration: null, progress: 0);
t.update(progress: 0.6);
t.update(progress: 1.0, message: 'Uploaded', duration: const Duration(seconds: 2));// Color the surface. iOS 26+ tints the glass (pass reduced alpha for subtlety);
// elsewhere (iOS 17–25 frosted, Reduce Transparency, Android) it fills the surface.
toast.show(
'Saved to Library',
style: const ToastStyleOverride(background: ToastColor(Color(0xFF1E1E2E))),
);
// Hex strings work too. Leave `foreground` null and a readable text color
// (near-black/near-white, per light/dark) is derived automatically by contrast.
toast.show(
'Copied',
style: ToastStyleOverride(background: ToastColor.hex('#b0afb0')),
);tint colors the accent (icon / spinner / progress ring); background colors
the surface; foreground overrides the (otherwise auto-derived) text color.
Each is a ToastColor(light, dark:) and resolves adaptively — no BuildContext.
toast.setDefaults(const LiquidToastsConfig(
defaultPosition: ToastPosition.topCenter,
maxLines: 3, // messages (per-toast maxLines still wins)
titleMaxLines: 2, // titles (per-toast titleMaxLines still wins)
safeArea: EdgeInsets.only(top: 96, bottom: 72),
maxVisible: 3,
));defaultDuration left null keeps the per-semantic defaults
(success/info/warning 3 s, error 4 s).
safeArea is a minimum inset in logical pixels. The native overlays always
respect the real device safe area too, taking the larger value at each edge, so
you can reserve space for app headers, floating controls, or bottom navigation
without double-counting the status bar or home indicator.
Name collision?
import 'package:liquid_toasts/liquid_toasts.dart' hide toast;and useToaster.instance.
| Type | Purpose |
|---|---|
toast (a Toaster) |
The primary API: callable show · success/error/warning/info/loading · raw(Toast) · promise · dismiss/dismissAll · setDefaults · queryGeometry · activeCount/activeIds — all shows return the handle synchronously |
Toast |
Immutable content (message, title, SF Symbol icon, semantic, style, position, duration, action, progress, haptic, …) with named constructors and copyWith |
ToastAction |
Single action button (label, onPressed, role, color) |
ToastHandle |
Live controller: patch-style update(...), replace(Toast), dismiss, onDismissed |
ToastStyleOverride / ToastColor |
Per-toast background (surface) / tint (accent) / foreground (text, auto-derived from background) / iconColor / glass / cornerRadius overrides (adaptive light/dark; ToastColor.hex(...) for hex) |
LiquidToastsConfig |
App-wide position/duration, message/title line limits, minimum safe area, and stack limits |
LiquidToasts / LoadingToast |
Deprecated legacy facade — working delegates until 1.0 |
The old static facade still works (with deprecation hints) and shares the same engine, so you can migrate incrementally:
| Before | After |
|---|---|
await LiquidToasts.success('Hi') |
toast.success('Hi') (no await) |
await LiquidToasts.show(Toast(...)) |
toast.raw(Toast(...)) |
LiquidToasts.showLoading(f, config: LoadingToast(...)) |
toast.promise(f, loading:, success:, error:) |
handle.update(Toast(...)) |
handle.replace(Toast(...)) — or patch: handle.update(progress: 0.6) |
LiquidToasts.dismissAll() |
toast.dismissAll() |
LiquidToasts.setDefaults(...) |
toast.setDefaults(...) |
The bundled example/ depends on the plugin via a path dependency. Flutter's
Swift Package Manager integration derives the package identity from the project
folder name, so the local checkout folder must be liquid_toasts (matching
the Dart package name), not liquid-toasts. Consumers installing from pub.dev
are unaffected. CocoaPods is also supported.
See LICENSE.