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.
Libraries
- fluent_bar
- A flexible, customizable, and feature-rich overlay notification library for Flutter applications with support for animated banners, progress tracking, haptic feedback, and custom popup strategies.