fluent_bar 1.0.0
fluent_bar: ^1.0.0 copied to clipboard
Powerful snackbar library for Flutter supporting smart queues, deduplication, pause on touch, Future actions with auto-loaders, and comprehensive analytics.
fluent_bar #
Notification bars for Flutter: snackbar, toast, banner, progress and loading. They queue up
instead of piling on, can be changed while they are on screen, and never need a BuildContext.
FluentBar.showSuccess('Profile saved');
Try the live demo: every feature has a screen you can play with, and each one shows the code behind it.
Each section below ends with two pointers:
- Demo - the screen in the live demo where this feature is shown, as Screen → Section. The
link opens the demo on that section. The code in brackets, for example
(queue/duplicates), is a search query for the demo app: type it into the search field there to find the same section yourself, for example on a phone. The search also finds sections by an API name, such aswithAction,clearQueueorduplicateComparator. - API - the reference page with every parameter (all classes).

Start here: Install · Set up · Show a bar · Recipes · Update a bar · Find and close · Progress
Going further: Regions and the queue · Duplicates · Overriding settings · Themes · Types · Builders · Global config · Accessibility · Lifecycle · Analytics · Errors · Good to know · Troubleshooting · Reference · Other packages · AI assistants · Example
Using an AI coding assistant? The package ships a full guide and ready-made rules for Cursor, Claude Code and
AGENTS.md, so the assistant writes code for the version you have installed. See Using with AI assistants.
Why fluent_bar #
- One call from anywhere. No
BuildContext, noScaffoldMessenger. Works from a bloc, a service, ormain()beforerunApp. - A queue, not a pile. One bar per region (top and bottom); the rest wait by priority.
- Replace anything. Content, position, transition, progress, button: each has a builder.
- Bars are alive. Change the text, type, progress, duration or position of a bar that is
already on screen. The same
idupdates instead of duplicating. - Progress built in. Linear or circular, a
Futurewrapper, and success / fail / cancel outcomes. - Themes and types. Six built-in themes, your own with three methods, and types that bundle the defaults you would otherwise repeat in every call.
- One rule for settings. The bar, then its type, then the global config. Everywhere.
- Your code cannot break it. A throwing callback, builder, theme or type is reported and replaced by a default; the bar still appears.
- Observable. Fifteen analytics events and per-bar metrics: time on screen, time in the queue, how the user interacted, why it closed.
- Considerate. Screen-reader live region, system "reduce motion", keyboard avoidance, pause on touch.
Install #
flutter pub add fluent_bar
Requires Flutter 3.32 or newer.
Set up once #
Bars live in an overlay above the navigator, so they survive route changes:
import 'package:fluent_bar/fluent_bar.dart';
MaterialApp(
builder: FluentBarScope.builder(),
home: const HomePage(),
);
Alternatives: navigatorObservers: [FluentBarObserver.instance], or
FluentBar.initialize(config: …, navigatorKey: yourKey).
To keep bars clear of your app bar and bottom navigation, wrap them once:
Scaffold(
appBar: FluentBarTopInset(child: AppBar(title: const Text('Inbox'))),
bottomNavigationBar: FluentBarBottomInset(child: NavigationBar(destinations: destinations)),
);
While another screen, a dialog or a sheet covers the screen, its insets are withdrawn; the on-screen keyboard is always taken into account, so bottom bars stay above it.
Demo: Quick start → Setup (
start/setup) · Behavior & gestures → Below the app bar, above the bottom bar (behavior/insets)
API:FluentBarScope·FluentBarObserver·FluentBarTopInset·FluentBarBottomInset
Show a bar #

Three ways, from shortest to most flexible:
// 1. One line.
FluentBar.showInfo('Copied to clipboard');
FluentBar.showError('Payment failed', title: 'Checkout');
// 2. A chain: a draft plus only what differs from the defaults.
FluentBar.success('Order #1042 has been shipped')
.withTitle('On its way')
.withAction('Track', onPressed: (controller, payload) => openTracking())
.withTimeoutBar()
.atBottom()
.show();
// 3. Everything by hand.
FluentBar.show<void>(
message: 'Ready',
type: FluentBarType.success,
options: const FluentBarOptions<void>(
behavior: FluentBarBehavior(position: FluentBarPosition.bottom),
),
);
Presets #

FluentBar.showSnack('Message archived', action: 'Undo', onAction: (controller, _) => restore());
FluentBar.showToast('Saved');
FluentBar.showBanner('Changes will sync later.', title: 'You are offline', type: FluentBarType.warning);
final entry = FluentBar.showLoading<void>('Connecting…');
Demo: Quick start → One-liners (
start/one-liners) · Quick start → The chain (start/the-chain) · Quick start → Presets (start/presets) · Playground (playground)
API:FluentBar·FluentBarItemBuilder·FluentBarOptions
Recipes #
The bars apps need most often. Copy, paste, change the text.
Saved #

FluentBar.showSuccess('Changes saved');
A hint that never piles up #

FluentBar.showToast('Copied', id: 'copied');
With an id, repeated taps update the toast on screen instead of queueing ten.
Undo #

FluentBar.showSnack('Conversation deleted', action: 'Undo', onAction: (controller, _) => restore());
Error with Retry #

FluentBar.error<void>('Could not load the feed')
.persistent()
.withAction('Retry', closeOnAction: false, onPressed: (controller, _) async {
await loadFeed(); // the button shows a loader meanwhile
controller.close();
})
.show();
With closeOnAction: false the bar stays while the work runs, with a loader on the button, and the
handler closes it. Without it a tap closes the bar at once and the handler runs after that.
Loading, then the result #

final loading = FluentBar.showLoading<void>('Signing in…', id: 'sign_in');
try {
await auth.signIn();
loading?.completeProgress(message: 'Welcome back', type: FluentBarType.success);
} catch (_) {
loading?.failProgress(message: 'Wrong email or password');
}
Upload with progress #

final upload = FluentBar.info<void>('Uploading 12 photos')
.withId('upload')
.withLinearProgress(value: 0, showText: true)
.atBottom()
.show();
upload?.updateFraction(3, 12);
upload?.completeProgress(message: 'All photos uploaded', type: FluentBarType.success);
Offline banner #

FluentBar.banner<void>('Changes will sync later.', title: 'You are offline', type: FluentBarType.warning, id: 'network').show();
// Later: the same id replaces the banner instead of adding a second bar.
FluentBar.success<void>('Back online').withId('network').withDuration(const Duration(seconds: 2)).show();
An alert that must be answered #

FluentBar.error<void>('Your session has expired.')
.withTitle('Sign in again')
.forced() // jumps the queue
.persistent() // no countdown
.undismissible() // no swipe, no tap, no close button
.withAction('Sign in', onPressed: (controller, _) => openSignIn())
.show();
Download with Cancel #

final entry = FluentBar.info<void>('Downloading the map')
.withId('download')
.withLinearProgress(value: 0, showText: true)
.withCancelButton(
'Cancel',
onCancel: () => download.cancel(), // the button shows a loader meanwhile
canceledMessage: 'Download cancelled',
canceledType: FluentBarType.warning,
)
.show();
download.onProgress = (received, total) => entry?.updateBytes(received, total);
withCancelButton is the action of the bar with the routine built in:
- On a tap the bar stops following the progress and the button shows a loader while
onCancelstops your task (the loader is visible only ifonCancelis asynchronous). Then the progress is cancelled and the bar leaves by its timer. - If
onCancelthrows, the error is reported and the bar follows the progress again. - If the task finishes or fails while it is being stopped, that result stays.
- What the stopped task still sends afterwards is ignored, progress and a failure alike: the
error a cancelled request ends with does not turn the bar red. To start over, show the bar again
with the same
id. - The button goes away with the progress, whatever the outcome: a finished upload does not
keep a Cancel button. Any action can do the same:
FluentBarActions(removeWhenProgressEnds: true).

A failure can offer a second try on the same bar. failProgress with retryLabel puts a Retry
button in place of Cancel and keeps the bar until the user answers:
void download() {
final entry = FluentBar.info<void>('Downloading the map')
.withId('map')
.withLinearProgress(value: 0, showText: true)
.withCancelButton('Cancel', onCancel: task.cancel, canceledMessage: 'Download cancelled')
.show();
task.start(
onProgress: (received, total) => entry?.updateBytes(received, total),
onDone: () => entry?.completeProgress(message: 'Map downloaded', type: FluentBarType.success),
onError: () => entry?.failProgress(
message: 'Download failed',
type: FluentBarType.warning, // an error by default
retryLabel: 'Retry',
onRetry: download, // the same id: the bar starts over in place
),
);
}
A tap on Retry removes the button and calls onRetry. Showing the bar again with the same id
and a progress starts it over in place: the usual type, the indicator and Cancel are back.
isPersistent: false lets the failed bar leave by its timer even with the button. The hold ends
with the answer: a tap on Retry, another button, a new progress or duration, or the same id shown again. retryLabel
and onRetry go together: with only one of them the bar fails as usual without a Retry button, and
an invalidCall warning goes to your error handler.
Demo: Progress & futures → Fail and retry (
progress/retry) · Progress & futures → Cancel button (progress/cancel-button)
Cookie consent #

FluentBar.info<void>('We use cookies to keep you signed in.')
.atBottom()
.persistent()
.undismissible()
.withoutIcon()
.withAction('Accept')
.show();
Achievement #

FluentBar.success<void>('7 days in a row. Keep going!')
.withTitle('Streak unlocked')
.withBackground(const Color(0xFF7C3AED))
.dark()
.withIcon(const Icon(Icons.emoji_events_rounded), color: Colors.amberAccent)
.withEntrance(FluentBarEntranceAnimation.elastic)
.atCenter()
.show()
?.heartbeat();
Label bars like these with .withPreset('undo') to tell them apart in logs and analytics.
Popular settings #
Every setting can be set on one bar or once for the whole app:
| I want… | On one bar | For the whole app (FluentBarConfig) |
|---|---|---|
| Bars at the bottom | .atBottom() |
defaultPosition: FluentBarPosition.bottom |
| A longer or shorter life | .withDuration(…) |
defaultDuration |
| A bar that stays until closed | .persistent() |
- |
| A countdown line (or none) | .withTimeoutBar(), .withoutTimeoutBar() |
defaultShowTimeoutBar |
| No swipe / no tap to close | .swipeable(false), .dismissOnTap(false), .undismissible() |
defaultIsSwipeEnabled, defaultDismissOnTap |
| A close button | .withCloseButton(), .withoutCloseButton() |
defaultShowCloseButton: true |
| Dark or light look | .dark(), .light() |
defaultThemeMode |
| Another theme | .withTheme(FluentBarTheme.minimal) |
defaultTheme |
| No icon / my icon | .withoutIcon(), .withIcon(…) |
defaultShowIcons |
| Another entrance | .withEntrance(FluentBarEntranceAnimation.fade) |
defaultEntranceAnimation |
| No animations at all | .withoutAnimations() |
defaultDisableAnimations: true |
| Vibration | .withHaptic(FluentBarHapticType.light) |
enableHaptics |
| A longer queue | - | maxQueueLength |
Demo: Quick start → Recipes (
start/recipes)
API:FluentBar·FluentBarItemBuilder
Change a bar that is on screen #

final entry = FluentBar.info<void>('Saving…').withId('save').persistent().show();
entry?.update(message: 'Uploading attachments');
entry?.controller.updateContent(type: FluentBarType.success, message: 'Saved');
entry?.controller.updateDuration(const Duration(seconds: 3), resetTimer: true);
entry?.patchBehavior((behavior) => behavior.copyWith(position: FluentBarPosition.bottom));
entry?.patchAppearance((appearance) => appearance.copyWith(backgroundColor: Colors.teal));
entry?.shake();
entry?.close();
// Without the entry at hand:
FluentBar.find('save')?.update(message: 'Saved');
FluentBar.close('save');
Changing the type or theme of a visible bar fades to the new look. A bar that is moved to another position fades in at its new place; if that region is busy it leaves, waits there and appears again.
Demo: Behavior & gestures → Control a live bar (
behavior/live-bar) · Behavior & gestures → Moving a bar that is already shown (behavior/relocation) · Behavior & gestures → Actions (behavior/actions) · Playground → Runtime control (playground/runtime)
API:FluentBarEntry·FluentBarItemActionsX·FluentBarController
Find and close #
Give a bar an id and you can reach it from anywhere, without keeping the entry around.
| I want to... | Call |
|---|---|
| Get a bar that is shown or waiting | FluentBar.find('save') - returns its entry or null |
| Check what is on screen / in the queue | FluentBar.activeItems, FluentBar.allQueue |
| Close one bar | FluentBar.close('save') or entry.close() |
| Close the bar that is on screen | FluentBar.closeActive() |
| Clear the queue and close the visible bars with their animation | FluentBar.closeAll() |
| Get / close the bars of one screen or feature | FluentBar.findByTag('checkout') / FluentBar.closeTag('checkout'), or FluentBarOwner |
| Drop the waiting bars, keep the visible ones | FluentBar.clearQueue() |
| Remove everything at once, no animation | FluentBar.dismissAndClear() |
// In another class, with only the id at hand:
if (FluentBar.find('save') != null) FluentBar.close('save');
// Every bar of one screen (see "Bars of one screen" in doc/queue.md):
FluentBar.closeTag('checkout');
Every call here, and every other call into the library, is safe inside initState, build and
dispose: the change happens at once, and widgets listening to the library are told right after
the current frame.
close returns false when there is no bar with that id. A bar closed from code reports the
dismiss reason manual. In widget tests call FluentBar.dismissAndClear() in tearDown.
Demo: Quick start → Closing bars (
start/closing) · Queue & duplicates → Clean up (queue/clean-up)
API:FluentBar·FluentBarEntry
Progress #

final entry = FluentBar.info<void>('Uploading photos')
.withId('upload')
.withLinearProgress(value: 0, showText: true)
.show();
entry?.updateValue(0.42); // 0.0 … 1.0
entry?.completeProgress(message: 'All photos uploaded', type: FluentBarType.success);
// or: entry?.failProgress(message: 'Upload failed');
// or: entry?.cancelProgress(fallbackMessage: 'Upload cancelled');
A bar with unfinished progress stays on screen; once it is finished the usual countdown starts.
cancelProgress on a bar whose progress has ended, or that has none, changes nothing.
A download or an upload reports bytes. updateBytes takes the same (received, total) pair that
network libraries give you, picks the unit for the text and copes with a missing size:
final download = FluentBar.info<void>('Downloading the map').withLinearProgress(showText: true).show();
download?.updateBytes(3250586, 52428800); // 3.1 / 50 MB
download?.updateBytes(3250586, null); // 3.1 MB and an indeterminate line: the size is unknown
More in Using fluent_bar with other packages.
Endless or not is decided by the value you pass, not by a setting. The theme only chooses the
shape - a line (withLinearProgress) or a circle (withCircularProgress):
| You pass | The indicator |
|---|---|
value(0.4), fraction(3, 8), bytes(received, total) |
fills up to that point |
indeterminate(), bytes(received, null) (also 0 or -1), or no value at all |
endless |
The latest value wins, so one bar can switch: start endless while the size is unknown and turn into a normal progress when it arrives, or the other way round.
In place of the icon (position: FluentBarProgressPosition.replaceIcon, the default of
withCircularProgress and also available to withLinearProgress) there is room for a percent
only, for a ring and for a line alike. A fraction, bytes and a labelBuilder
text show their percent there; bytes without a total show no text.
For a Future, one call does it all and hands the result back to you:
final user = await FluentBar.future<User>(
api.saveProfile(),
loading: 'Saving…',
success: (user) => 'Profile of ${user.name} saved',
error: (error) => 'Could not save',
);
Demo: Progress & futures → Three ways to update (
progress/update) · Progress & futures → Cancel button (progress/cancel-button) · Progress & futures → Finishing a progress (progress/finish) · Progress & futures → FluentBar.future (progress/future) · Progress & futures → Downloads in bytes (progress/bytes) · Progress & futures → Look of the indicator (progress/look)
API:FluentBarProgressState·FluentBarProgressTheme
Everything above is enough for most apps. The rest is listed below.
More in the library #
The full documentation is in doc/:
- Queue and duplicates: regions, priorities, the queue strategies, duplicate rules, tags for one screen
- Behavior: the close button, the lifecycle of a bar, and the things that follow from how bars work
- Settings: global config, and the order in which every setting is resolved
- Themes: built-in themes, tweaking one, light and dark, custom themes
- Types: built-in types, variations,
typeOverrides, custom types - Builders: replace the content, position, transition or progress of a bar
- Accessibility: screen readers, reduce motion, and progress announcements
- Analytics: events, metrics, and sending them to an analytics service
- Errors: every error type and severity, when it is reported and what the library does then
- Troubleshooting: a symptom, why it happens and what to do
- Reference: every public class, method and enum
Using with other packages #
fluent_bar needs no other package of yours (its own dependencies are Flutter, cupertino_icons
and meta). Copy-ready examples of using it together with popular
packages are in doc/INTEGRATIONS.md:
| For | Packages |
|---|---|
| Download and upload progress | dio, http, firebase_storage |
| Showing bars from your state layer | flutter_bloc, provider, flutter_riverpod |
| Sending bar events to analytics | firebase_analytics |
| Sending isolated errors to crash reports | firebase_crashlytics, sentry_flutter |
| An offline banner | connectivity_plus |
| Pushes that arrive while the app is open | firebase_messaging |
| Setting up the overlay with a router | go_router |
Using with AI assistants #
The package ships a reference written for AI coding assistants, so the code they write matches the version you have installed.
AI_GUIDE.md holds the rules and an
index; each file in doc/ai/ covers one
topic, so an assistant reads only what its task needs. Both are inside the package you installed.
ai/AGENTS.md is a short text that
tells an assistant where to find them:
| You use | Do this |
|---|---|
Cursor, Codex, Copilot agents, Gemini CLI, Windsurf and other agents that read AGENTS.md |
Append ai/AGENTS.md to the AGENTS.md in your project root. For GitHub Copilot it also works in .github/copilot-instructions.md, for Gemini CLI in GEMINI.md. |
| Claude Code | The same, then add the line @AGENTS.md to the CLAUDE.md in your project root. |
| A chat assistant without access to your project | Attach AI_GUIDE.md and the files from doc/ai/ your question is about. |
| A tool that fetches docs by URL | Point it at llms.txt. |
Example #
The example app is the
live demo: a playground that generates the chain for the
bar you configure, a queue monitor, and a screen per feature. Run it locally with:
cd example
flutter run
Contributing #
Issues and pull requests are welcome. See CONTRIBUTING.md.
Acknowledgements #
Built with the help of Claude by Anthropic.
If the package saves you time, you can buy me a coffee on Ko-fi.
License #
See LICENSE.
