Tabs across the top — swipe or tap between them — are how apps split a screen into a few related views: a profile's Posts, Media, and Likes, or a store's Overview and Reviews. Flutter gives you TabBar for the row of tabs and TabBarView for the swipeable pages, kept in sync by a TabController. This guide covers the quick DefaultTabController approach, when you need a manual controller, scrollable tabs, and the length-mismatch error everyone hits once, with paste-ready code.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowEvery snippet below is paste-ready against current stable Flutter. Tabs sit in the AppBar's bottom slot and complement, rather than replace, the bottom navigation bar — top tabs split one section, the bottom bar splits the app.
We'll build tabs with DefaultTabController, switch to a manual TabController when we need to listen, make the tabs scrollable, and fix the length-mismatch assertion.
If you'd rather watch a tabbed profile screen built and styled, the channel covers these layouts inside real apps.
The easy way: DefaultTabController
For self-contained tabs, DefaultTabController creates and shares the controller for you. Set its length, drop a TabBar in the AppBar's bottom slot and a TabBarView in the body, and tapping and swiping stay in sync automatically.
DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Profile'),
bottom: const TabBar(
tabs: [
Tab(text: 'Posts'),
Tab(text: 'Media'),
Tab(text: 'Likes'),
],
),
),
body: const TabBarView(
children: [PostsTab(), MediaTab(), LikesTab()],
),
),
)
That's the whole pattern. The TabBar and TabBarView find the shared controller through the build context, so a tap scrolls the views and a swipe highlights the tab — no manual synchronisation.
When you need a manual TabController
Reach for a manual TabController when you must react to tab changes — load data when a tab opens, or drive the selection from a button. Create it in a StatefulWidget with SingleTickerProviderStateMixin, and dispose it.
class _ProfileState extends State<Profile>
with SingleTickerProviderStateMixin {
late final TabController _controller =
TabController(length: 3, vsync: this);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(controller: _controller, tabs: const [/* ... */]),
),
body: TabBarView(controller: _controller, children: const [/* ... */]),
);
}
}
Pass the same _controller to both the bar and the views so they stay in lockstep, and call _controller.animateTo(index) to switch tabs from code. Disposing the controller in dispose is what prevents a memory leak.

Build polished, tabbed UIs
The Complete Flutter Guide covers tabs, navigation, and state from first principles to shipped apps.
Enrol nowScrollable tabs
By default the tabs spread to fill the width, which crowds them once you have many. Set isScrollable: true and each tab sizes to its label while the row scrolls horizontally — ideal for category bars.
TabBar(
isScrollable: true,
tabs: categories.map((c) => Tab(text: c)).toList(),
)
This is the right mode for a news app's sections or a shop's many departments. For three or four short labels, leave it off so the tabs fill the bar evenly.
Styling the tabs
TabBar exposes indicatorColor, indicatorWeight, and labelColor / unselectedLabelColor, plus a custom indicator for pill or rounded styles. On Material 3 the defaults already follow your theme, so you often need only a colour tweak rather than a full custom indicator.
Common mistakes
- Length not matching the tabs and views. The controller
length, theTabcount, and theTabBarViewchildren must all be equal. - Forgetting to dispose a manual TabController. Always dispose it to avoid a leak.
- Using two controllers. The bar and the views must share one controller, or they desync.
- Crowding many tabs without isScrollable. Turn it on for six or more tabs.
- Confusing tabs with bottom navigation. Tabs split a section; the bottom bar splits the app.
Frequently asked questions
How do I create tabs in Flutter?
Wrap the screen in a DefaultTabController, put a TabBar in the AppBar's bottom slot and a matching TabBarView in the body; they sync automatically.
What is the difference between DefaultTabController and TabController in Flutter?
DefaultTabController provides a controller for you; a manual TabController is for when you must listen to or drive the selection from code.
How do I make Flutter tabs scrollable?
Set isScrollable: true on the TabBar so each tab sizes to its label and the row scrolls horizontally — best for many tabs.
Why do my Flutter tabs throw a length mismatch error?
The controller's length must equal both the Tab count and the TabBarView children count. Keep all three identical.
Further reads
Keep going with the tutorials that pair with this guide:
- Flutter Development Guide 2026 — the full Flutter hub.
- Flutter BottomNavigationBar — the bottom-bar counterpart to top tabs.
- Flutter Navigation Basics — push, pop, and named routes.
- Flutter AppBar Customisation — the bottom slot tabs live in.
- Flutter Drawer — the side-menu navigation pattern.
Sources: Flutter documentation — TabBar, TabBarView, TabController, DefaultTabController, and the "Work with tabs" cookbook recipe (docs.flutter.dev). Verified against current stable Flutter.