Skip to content

Repository files navigation

liquid_toasts

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.

Showcase

stacking.mp4

Stacking — staggered in and out
variable-color.mp4

Animated icons — SF Symbol effects
progress.mp4

Progress — determinate upload
action.mp4

Action button — inline Undo

Highlights

  • 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 glassEffect Liquid Glass on iOS 26+, a frosted .ultraThinMaterial fallback 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 stylessuccess / 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-26 drawOn) 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 / activeIds introspection.

Requirements

  • 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+.

Platform support

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.

Install

dependencies:
  liquid_toasts: ^0.5.0

Usage

import '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 callable

Action button

toast.warning(
  'Low storage',
  duration: null, // persistent until tapped/dismissed
  action: ToastAction(
    label: 'Manage',
    role: ToastActionRole.primary,
    onPressed: openStorageSettings,
  ),
);

Wrap a future (the call returns your result)

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) => …).

Persistent toast + live handle

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 completes

update patches only the fields you pass (rapid patches compose, in order); replace(Toast(...)) swaps the content wholesale.

Positioning, replace-by-key, progress

// 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));

Custom colors

// 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.

App-wide defaults

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 use Toaster.instance.

API at a glance

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

Migrating from LiquidToasts

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(...)

Notes for contributors

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.

License

See LICENSE.

About

A Flutter plugin to show toasts (iOS + Android)

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages