zentoast 1.0.0
zentoast: ^1.0.0 copied to clipboard
A headless, fully customizable toast system for Flutter.

zentoast #
A headless, fully customizable toast system for Flutter. You design the UI β zentoast takes care of animation, physics, queuing, gestures, and multi-position viewers. Perfect for building Sonner-like toasts, message banners, or fully custom notification UIs.

Features #
- β¨ Headless Architecture β Bring your own widgets & design
- π― Flexible Positioning β Display toasts anywhere on screen
- π¨ Extremely Customizable β Full control over layout, styling & behavior
- π Fluid Animations β Motor-powered, physics-based animation system
- π Rich Gestures β Drag to dismiss, tap to pause, swipe interactions
- π§ Theming Support β Global settings via
ToastTheme - π¦ Multiple Viewers β Independent stacks with synchronized smoothness
Installation #
Requires Dart 3.13 or newer (below 4.0) and Flutter 3.47 or newer.
Add to your pubspec.yaml:
dependencies:
zentoast: ^1.0.0
Import:
import 'package:zentoast/zentoast.dart';
Quick Start #
Wrap your app with ToastProvider and configure a ToastViewer:
void main() {
runApp(
ToastProvider.create(
child: MyApp(),
),
);
}
Minimal Example #
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => ToastThemeProvider(
data: ToastTheme(
gap: 8,
viewerPadding: EdgeInsets.all(12),
),
child: Stack(
children: [
Positioned.fill(child: child ?? SizedBox()),
SafeArea(
child: ToastViewer(
alignment: Alignment.topRight,
delay: Duration(seconds: 3),
visibleCount: 3,
),
),
],
),
),
home: HomePage(),
);
}
}
Motion configuration #
Motion is grouped by purpose: enter, exit, reposition (stack changes and
expand/collapse), and dragReturn. Position, scale and opacity use independent
motions; drag return exposes position and opacity. Direct dragging always follows
the finger.
Set app defaults with ToastTheme.motion, or override the entire configuration
for one ToastViewer. Use copyWith to override only a phase or property:
import 'package:flutter/widgets.dart';
import 'package:motor/motor.dart';
import 'package:zentoast/zentoast.dart';
final motion = const ToastMotionConfig().copyWith(
exit: const ToastMotionTransition(
position: Motion.curved(Duration(milliseconds: 300), Curves.easeOut),
scale: Motion.curved(Duration(milliseconds: 300), Curves.easeOut),
opacity: Motion.linear(Duration(milliseconds: 200)),
),
);
ToastThemeProvider(
data: ToastTheme(
viewerPadding: const EdgeInsets.all(12),
gap: 8,
motion: motion,
),
child: const ToastViewer(delay: null),
);
// Viewer-specific policy, overriding its theme:
const ToastViewer(
motion: ToastMotionConfig(policy: ToastMotionPolicy.reduced),
);
The default system policy follows MediaQuery.disableAnimations. Without a
MediaQuery, it uses full motion. full always uses configured motion, reduced
snaps automatic position/scale changes and fades over 150 ms with Curves.easeOut,
and none applies every target immediately. Reduced motion keeps direct dragging
but snaps back on return. Policy changes also apply to animations already running.
Full-motion defaults retain the 300 ms entrance hold and original snappy springs
with a 500 ms perceptual duration. Spring settling can take longer. Dismissed toasts
are removed only after every matching mounted viewer's exit controllers finish;
a missing or unmounted viewer does not prevent cleanup. Custom motions must reach
their target and finish: Motor's Motion.none holds its starting value, so use
ToastMotionPolicy.none to disable animation. Theme motion configurations switch
discretely halfway through theme interpolation; spacing still interpolates.
Triggering a Toast #
ElevatedButton(
onPressed: () {
Toast(
height: 64,
builder: (toast) => Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.check, color: Colors.white),
SizedBox(width: 12),
Expanded(
child: Text(
'Success! Your changes have been saved.',
style: TextStyle(color: Colors.white),
),
),
IconButton(
icon: Icon(Icons.close, color: Colors.white),
onPressed: () => toast.hide(context),
),
],
),
),
).show(context);
},
child: Text('Show Toast'),
)
Building Your Own Toast UI #
zentoast is headless, meaning you provide the UI.
Hereβs a custom toast example:
class CustomToast extends StatelessWidget {
const CustomToast({
super.key,
required this.title,
required this.message,
required this.onClose,
this.icon,
this.color = Colors.blue,
});
final String title;
final String message;
final VoidCallback onClose;
final IconData? icon;
final Color color;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: Offset(0, 4),
),
],
border: Border(
left: BorderSide(color: color, width: 4),
),
),
child: Row(
children: [
if (icon != null) ...[
Icon(icon, color: color),
SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
)),
SizedBox(height: 4),
Text(message,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
)),
],
),
),
IconButton(
icon: Icon(Icons.close, size: 20),
onPressed: onClose,
),
],
),
),
);
}
}
Usage:
Toast(
height: 80,
builder: (toast) => CustomToast(
title: 'New Message',
message: 'You have received a new message from John',
icon: Icons.message,
color: Colors.purple,
onClose: () => toast.hide(context),
),
).show(context);
Positioning #
Place toasts anywhere with alignment:
ToastViewer(alignment: Alignment.topLeft)
ToastViewer(alignment: Alignment.bottomCenter)
ToastViewer(alignment: Alignment.topRight)
ToastViewer(alignment: Alignment.bottomRight)
Multiple Viewers #
You can show independent toasts in multiple corners for each toast category:
Stack(
children: [
Positioned.fill(child: child),
SafeArea(
child: ToastViewer(
alignment: Alignment.topRight,
delay: Duration(seconds: 3),
// Display all toast no filter
categories: null,
),
),
SafeArea(
child: ToastViewer(
alignment: Alignment.bottomCenter,
delay: Duration(seconds: 5),
// Display only `Toast` with specific category
categories: [
ToastCategory.success,
ToastCategory.error,
],
),
),
],
)
Animations stay smooth even when dismissing multiple stacks simultaneously.
Categorize Toast #
Organize notifications by ToastCategory so each ToastViewer can focus on
the messages it cares about. Every toast defaults to ToastCategory.general,
and you can introduce new categories with a simple const ToastCategory('name').
const billingCategory = ToastCategory('billing');
void _showCategorizedToasts(BuildContext context) {
Toast(
category: ToastCategory.success,
builder: (toast) => SuccessToast(onClose: () => toast.hide(context)),
).show(context);
Toast(
category: ToastCategory.error,
builder: (toast) => ErrorToast(onClose: () => toast.hide(context)),
).show(context);
Toast(
category: billingCategory,
builder: (toast) => BillingToast(onClose: () => toast.hide(context)),
).show(context);
}
Widget build(BuildContext context) {
return ToastProvider.create(
child: Stack(
children: [
ToastViewer(
alignment: Alignment.topRight,
categories: const [
ToastCategory.success,
ToastCategory.error,
],
),
ToastViewer(
alignment: Alignment.bottomLeft,
categories: const [billingCategory],
delay: const Duration(milliseconds: 300),
),
],
),
);
}
With this setup, success and error notifications render at the top-right, while billing alerts stay anchored at the bottom-left with a custom delay.
Theming #
Zentoast's own code uses Flutter widgets without importing Material. Motor 1.1.0
still imports flutter/material.dart through its public exports, so removing
Material from the entire import graph also requires an upstream Motor change
or replacing that dependency. Wrap your app or a subtree in ToastThemeProvider,
then read the nearest configuration with
ToastThemeProvider.of(context). Without a provider, viewers use
ToastTheme.kDefault.
ToastTheme is no longer a ThemeExtension; move toast configuration from
ThemeData.extensions into ToastThemeProvider. The styled UI examples use
Material components supplied by the consuming app; see example/ for an app
using package:material_ui/material_ui.dart.
ToastThemeProvider.data is now non-nullable. Omit it or pass
ToastTheme.kDefault to use the defaults.
ToastThemeProvider(
data: ToastTheme(
gap: 12,
viewerPadding: EdgeInsets.all(16),
),
child: YourApp(),
)
Advanced Configuration #
Toast(
height: 100,
category: ToastCategory.success, // Config customize category
builder: (toast) => YourToastWidget(
onClose: () => toast.hide(context),
),
);
ToastViewer(
alignment: Alignment.topRight,
delay: Duration(seconds: 4),
visibleCount: 3,
categories: [ToastCategory.success, ToastCategory('card')],
);
Gesture Support #
zentoast includes gesture interaction with no extra setup:
- Swipe to dismiss (vertical)
- Touch to pause auto-dismiss
- Drag to remove
- Smooth physics response powered by motor
Example App #
See /example for:
- Sonner-like toasts
- Brutalist / Card variants
- Multi-position demos
- Gesture demos
- Advanced theming and animations
API Overview #
Core Classes #
Toastβ Creates a toast instanceToastProviderβ Global manager for the toast stackToastViewerβ Renders a toast queue with animationsToastThemeβ Global styling configToastThemeProviderβ Provides theme to descendants
Key Methods #
Toast.show(context)β Show a toastToast.hide(context)β Hide the toastToastProvider.of(context)β Access provider manually
Contributing #
Contributions are welcome! Please open an issue or submit a PR.
License #
MIT License. See LICENSE for details.