Learn how to create a persistent TabBar for your Flutter app with zero dependencies.
Intro
Flutter offers many beautiful components out-of-the-box. Even though you can customize a fair amount of settings, you may sometimes reach a point where you just have to build your own.
A "Persistent Bottom TabBar" is a common approach for a layout seen in many apps. In this guide we will explore how to create your own and customize it to your needs. It's easier than you might think!
Setup
To create a Persistent Bottom TabBar you need to include it in a top-level screen so that every other page would be a child of your Layout. We will create a main_navigation.dart
to create this component.
Let's start by creating a global Scaffold
widget with a custom bottomNavigationBar
that includes a child DefaultTabController
.
Main Scaffold setup
class MainNavigation extends StatelessWidget {
final Widget child;
const MainNavigation({required this.child, super.key});
@override
Widget build(BuildContext context) {
final List<Widget> tabs = [
const Tab(icon: Icon(CustomIcons.search)),
const Tab(icon: Icon(CustomIcons.star)),
const Tab(icon: Icon(CustomIcons.calendar)),
const Tab(icon: Icon(CustomIcons.user)),
];
return Scaffold(
primary: true,
body: child,
bottomNavigationBar: Material(
color: Colors.white,
shape: Border(
top: BorderSide(color: Colors.grey.withOpacity(0.4), width: 1),
),
borderOnForeground: false,
child: SafeArea(
bottom: true,
child: DefaultTabController(
length: tabs.length,
child: TabBar(
splashBorderRadius: BorderRadius.zero,
dividerHeight: 0,
dividerColor: Colors.white,
indicatorPadding: const EdgeInsets.symmetric(horizontal: 5),
indicatorSize: TabBarIndicatorSize.tab,
tabs: tabs,
),
),
),
),
);
}
}
The Scaffold's bottomNavigationBar
is a wrapper in a Material
class which enables Material features like Clipping, Elevation and Tap Effects. Here we can customize the background color and shape of our TabBar container. Following, we have the DefaultTabController
wrapped an a SafeArea
widget to safely place our TabBar above any system UI elements.
Our MainNavigation
expects a child widget to render as the main Scaffold
body. This child widget should be dynamic base on your routing or any other mechanism. For example if you are using GoRouter
's ShellRoute
you can do the following.
return GoRouter(
initialLocation: "/",
routes: [
ShellRoute(
builder: (context, state, child) =>
MainNavigationTabs(child: child),
...
)
]
);
Adding an Indicator for active tab
Now that we have a basic layout working we can customize further our TabBar
. Let's add a line as an indicator of the currently active screen.
For that we can extend the BoxPainter
component and customize it.
class TopIndicator extends Decoration {
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _TopIndicatorBox();
}
}
class _TopIndicatorBox extends BoxPainter {
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
Paint paint = Paint()
..color = Colors.red
..strokeWidth = 3
..isAntiAlias = true;
canvas.drawLine(offset, Offset(cfg.size!.width + offset.dx, 0), paint);
}
}
The example above will draw a red line with 3px stroke width above the active TabBar item. We need to add it to our TabBar component we created earlier.
Scaffold(
child: SafeArea(
child: TabBar(
...
+ indicator: TopIndicator(),
),
),
)
Final Result
With that we have now created a custom Tabs layout which we can customize easily. Here is the final result.
class MainNavigation extends StatelessWidget {
final Widget child;
const MainNavigation({required this.child, super.key});
@override
Widget build(BuildContext context) {
final List<Widget> tabs = [
const Tab(icon: Icon(CustomIcons.search)),
const Tab(icon: Icon(CustomIcons.star)),
const Tab(icon: Icon(CustomIcons.calendar)),
const Tab(icon: Icon(CustomIcons.user)),
];
return Scaffold(
primary: true,
body: child,
bottomNavigationBar: Material(
color: Colors.white,
shape: Border(
top: BorderSide(color: Colors.grey.withOpacity(0.4), width: 1),
),
borderOnForeground: false,
child: SafeArea(
bottom: true,
child: DefaultTabController(
length: tabs.length,
child: TabBar(
splashBorderRadius: BorderRadius.zero,
dividerHeight: 0,
dividerColor: Colors.white,
indicatorPadding: const EdgeInsets.symmetric(horizontal: 5),
indicatorSize: TabBarIndicatorSize.tab,
tabs: tabs,
indicator: TopIndicator(),
),
),
),
),
);
}
}
class TopIndicator extends Decoration {
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _TopIndicatorBox();
}
}
class _TopIndicatorBox extends BoxPainter {
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
Paint paint = Paint()
..color = Colors.red
..strokeWidth = 3
..isAntiAlias = true;
canvas.drawLine(offset, Offset(cfg.size!.width + offset.dx, 0), paint);
}
}