An advanced, strongly-typed navigation and overlay management system for Flutter, built with zero external UI dependencies on pure package:flutter/widgets.dart.
- Centralized Route Management: Define all application routes in a single declarative registry.
- Strongly-Typed Route Arguments: Pass type-safe arguments using
RouteParamswith built-in validation and polymorphic type resolution. - Custom & Adaptive Transitions: Seamlessly apply transitions (slide, fade, scale, etc.) per route or globally based on platform/path.
- Versatile Presentation Styles: Present any screen as a full page, a modal dialog, or a swipeable bottom sheet via
TraceRoute. - Semantic Navigation API: Create a decoupled, domain-driven navigation layer using
Traceobjects. - Reactive Navigation Lifecycle: Listen to visibility changes (
onAppear,onDisappear) viaBrowser.watch. - Atomic Argument Consumption: Eliminate duplicate event triggers on widget rebuilds with
context.getArgumentAndClean<T>(). - Universal Navigation & Deep Linking: Automatic URL query parameter extraction to
DeepLinkParamand versatile URL routing withcontext.launchAction(). - Advanced Overlays & Sequential Banners: Managed banner queues, modal overlays, and bottom sheets decoupled from the navigator stack.
- Code Splitting & Deferred Loading: Out-of-the-box support for lazy loading routes with
DeferredBrowserRoute. - Zero UI Framework Dependencies: 100% decoupled from Material and Cupertino widgets.
Add browser_router to your pubspec.yaml:
dependencies:
browser_router: ^0.3.0Then run:
flutter pub getCreate a list of BrowserRoute instances:
import 'package:browser_router/browser.dart';
import 'package:flutter/widgets.dart';
final routes = [
BrowserRoute(
path: '/',
page: const HomeScreen(),
routeTransition: RouteTransition.none,
),
BrowserRoute(
path: '/profile',
page: const ProfileScreen(),
routeTransition: RouteTransition.slide_right,
),
];Place Browser at the root of your application widget hierarchy:
import 'package:browser_router/browser.dart';
import 'package:flutter/widgets.dart';
import 'routes.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Browser(
routes: routes,
defaultRoute: routes.first,
builder: (context, routeObserver, generate) {
return WidgetsApp(
color: const Color(0xFFFFFFFF),
navigatorObservers: [routeObserver],
onGenerateRoute: generate,
onGenerateInitialRoutes: (routePath) => [
generate(
RouteSettings(name: routePath, arguments: const <dynamic, dynamic>{}),
),
],
);
},
);
}
}Navigate using the BuildContext extension methods:
// Push a new route
context.pushNamed('/profile');
// Pop the current route
context.pop();Pass strongly-typed data between screens safely without casting dynamic maps.
Subclass RouteParams (using final class or base class):
import 'package:browser_router/browser.dart';
final class ProfileArgs extends RouteParams {
const ProfileArgs({required this.userId});
final String userId;
@override
bool validate() => userId.isNotEmpty;
}Enforce arguments validation before navigation occurs. If validation fails, Browser automatically falls back to defaultRoute:
BrowserRoute(
path: '/profile',
page: const ProfileScreen(),
validateArguments: (check, get) => check<ProfileArgs>(),
)context.pushNamed(
'/profile',
args: [ProfileArgs(userId: 'usr_12345')],
);Use context.getArgument<T>() in your build method. This read is idempotent and safe across multiple widget rebuilds:
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final args = context.getArgument<ProfileArgs>();
return Center(
child: Text('User ID: ${args?.userId}'),
);
}
}browser_router solves the problem of lost return data and unhandled gestures (swipe-to-dismiss) by updating the route settings of the underlying screen directly.
// Return data directly when popping
context.pop(args: OrderResultArgs(status: 'COMPLETED'));
// Or pop multiple screens to the root and pass arguments
context.popToFirst(args: [OrderResultArgs(status: 'COMPLETED')]);If a screen can be dismissed via swipe gestures or system back buttons, stage return arguments in initState or upon user actions using setPopArgument:
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.setPopArgument(DraftSavedArgs(savedAt: DateTime.now()));
});
}Wrap the receiving widget with Browser.watch. In onAppear, use context.getArgumentAndClean<T>() to read and atomically remove the argument:
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Browser.watch(
onAppear: (context, deepLink) {
// Atomic consumption: eliminates repeat execution on rebuilds
final result = context.getArgumentAndClean<OrderResultArgs>();
if (result != null) {
showToast('Order: ${result.status}');
}
},
child: const HomeContent(),
);
}
}| Method | Behavior | Primary Use Case |
|---|---|---|
context.getArgument<T>() |
Reads the argument without modifying the route map. | Screen construction data (e.g. IDs, configurations). |
context.getArgumentAndClean<T>() |
Reads and removes the argument from the route map. | One-time events (e.g. pop results, snackbar triggers). |
Encapsulate routes, arguments, and presentations into reusable domain objects:
enum AppPath {
home('/'),
profile('/profile'),
productDetail('/product/detail');
const AppPath(this.path);
final String path;
}
class AppTrace extends Trace {
const AppTrace._({
required super.path,
super.args,
super.traceRoute,
});
factory AppTrace.toProfile(String userId) {
return AppTrace._(
path: AppPath.profile.path,
args: ProfileArgs(userId: userId),
traceRoute: const PageTraceRoute(
routeTransition: RouteTransition.slide_right,
),
);
}
factory AppTrace.toProductModal(String productId) {
return AppTrace._(
path: AppPath.productDetail.path,
args: ProductArgs(id: productId),
traceRoute: const PopupTraceRoute(
routeTransition: RouteTransition.fade,
),
);
}
}// Standard push
AppTrace.toProfile('123').push(context);
// Push and replace current route
AppTrace.toProfile('123').pushAndReplacement(context);
// Pop to first screen and push
AppTrace.toProfile('123').popToFirstAndPush(context);
// Pop to root and replace
AppTrace.toProfile('123').cleanAndPush(context);
// Pop to existing instance in stack or push if not present
AppTrace.toProfile('123').findMeOrPush(context);Change how a route is presented without altering its widget implementation:
PageTraceRoute: Full-screen page navigation.PopupTraceRoute: Displays the route as a modal dialog.SwipeTraceRoute: Displays the route as an interactive bottom sheet with swipe-to-dismiss gestures.OverlayTraceRoute: Displays the route in the overlay layer.
browser_router includes modern Material 3, iOS Cupertino, Web, and legacy presets:
| Preset | Platform / Style | Motion Behavior |
|---|---|---|
RouteTransition.fade_through |
Material 3 & Web | Outgoing fades out & scales (1.0 -> 0.96), incoming fades in & scales (0.92 -> 1.0). Ideal for bottom nav bars and top-level destinations. |
RouteTransition.fade_scale |
Modern Web & M3 | Snappy zoom-fade (0.95 -> 1.0) with fast easing. Perfect for web SPAs, search overlays, and dialogs. |
RouteTransition.shared_axis_x |
Material 3 | Horizontal directional slide with subtle fade. Ideal for wizards and linear multi-step flows. |
RouteTransition.shared_axis_y |
Material 3 | Vertical directional slide with subtle fade. Ideal for form expansions and vertical progressions. |
RouteTransition.shared_axis_z |
Material 3 | Depth zoom (0.8 -> 1.0 / 1.0 -> 1.1) with fade. Ideal for drill-down hierarchies. |
RouteTransition.slide_cupertino |
Authentic iOS | iOS native push with left-edge gradient drop shadow and 1/3 parallax on the exiting route. |
RouteTransition.scale |
Popup / Dialog | Clean scale and fade animation for alerts and confirmation modals. |
RouteTransition.slide_right |
Legacy Slide | Standard horizontal right slide. |
RouteTransition.slide_left |
Legacy Slide | Standard horizontal left slide. |
RouteTransition.slide_up |
Legacy Slide | Standard vertical upward slide. |
RouteTransition.slide_down |
Legacy Slide | Standard vertical downward slide. |
RouteTransition.fade |
Legacy Fade | Simple fade in / fade out. |
RouteTransition.none |
Instant | Zero-duration transition without animation. |
You can define custom transition builders at the route level or per navigation request:
// At BrowserRoute level
BrowserRoute(
path: '/custom',
page: const CustomScreen(),
customTransition: CustomBuildTransition(
({required animation, required secondaryAnimation, required child}) {
return RotationTransition(
turns: animation,
child: child,
);
},
),
);
// Or per navigation request via TraceRoute
context.pushNamed(
'/custom',
traceRoute: PageTraceRoute(
customTransition: CustomBuildTransition(
({required animation, required secondaryAnimation, required child}) {
return FadeTransition(opacity: animation, child: child);
},
),
),
);All BrowserPageRoute and BrowserPopupRoute transitions automatically detect system accessibility settings:
MediaQuery.disableAnimationsOf(context)MediaQuery.accessibleNavigationOf(context)
When motion reduction is requested by the user, transitions bypass animations instantly with zero motion discomfort, requiring zero extra configuration.
Configure global platform-adaptive transition rules using built-in Browser.defaultAdaptiveTransition:
Browser(
routes: routes,
defaultRoute: routes.first,
adaptiveTransition: Browser.defaultAdaptiveTransition, // iOS: slide_cupertino, Android: shared_axis_x, Web/Desktop: fade_scale
adaptiveTrace: (name) {
// All routes under /modal/ open as popups automatically
if (name?.startsWith('/modal/') ?? false) {
return const PopupTraceRoute();
}
return null;
},
builder: (context, routeObserver, generate) => ...,
)browser_router automatically captures query parameters into DeepLinkParam:
// Navigating to: /profile?id=456&theme=dark
final deepLink = context.getArgument<DeepLinkParam>();
final id = deepLink?.params['id']; // "456"Use context.launchAction() for unified routing of internal routes and external URLs:
// Pop current view
context.launchAction('/?navigateType=pop');
// Pop to first view and push
context.launchAction('/profile?navigateType=popFirstAndPush');
// Push replacement
context.launchAction('/dashboard?navigateType=pushReplacement');
// External link (triggers openUrl callback)
context.launchAction('https://flutter.dev');Manage UI components that sit above the navigation stack using built-in overlay utilities:
Display notification banners one after another in FIFO order:
Browser.enqueueBanner(
context,
(dismiss) => Container(
padding: const EdgeInsets.all(16),
color: const Color(0xFF008080),
child: Row(
children: [
const Text('Update available!'),
GestureDetector(
onTap: dismiss,
child: const Text(' Dismiss'),
),
],
),
),
);Show a modal overlay independent of the Navigator route stack:
Browser.showOverlay(
context,
backgroundColor: const Color(0x80000000),
isDismissible: true,
builder: (dismiss) => Container(
width: 300,
height: 200,
color: const Color(0xFFFFFFFF),
child: Center(
child: GestureDetector(
onTap: dismiss,
child: const Text('Close Modal'),
),
),
),
);
// Dismiss programmatically
Browser.dismissOverlay(context);Browser.showModalBottomSheet(
context: context,
backgroundColor: const Color(0xFFFFFFFF),
heightFactor: 0.6,
builder: (context) => const SheetContentView(),
);Optimize initial download bundle sizes on Flutter Web and apps by loading route modules on-demand:
import 'package:browser_router/deferred_browser_route.dart';
import 'package:my_app/screens/heavy_feature.dart' deferred as heavy_feature;
final routes = [
DeferredBrowserRoute(
path: '/heavy_feature',
loadLibrary: heavy_feature.loadLibrary,
pageBuilder: () => heavy_feature.HeavyFeatureScreen(),
loadingWidget: const Center(child: Text('Loading...')),
),
];browser_router supports both Hash-based and Path-based URL routing strategies.
URLs contain #: https://yourapp.com/#/profile?id=123. Works out-of-the-box without web server configuration.
Clean URLs: https://yourapp.com/profile?id=123.
To enable:
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy();
runApp(const MyApp());
}Important
When using path-based URLs, configure your web server (Nginx, Firebase Hosting, Apache) to rewrite all requests to index.html to prevent 404 errors on direct URL access.
This project is licensed under the MIT License - see the LICENSE file for details.
Big thanks to the contributors:
![]() Eduardo Martínez Catalá |
![]() Cayetano Bañón Rubio |
![]() Jesus Bernabeu |


