bottom_tabs
| Package | Kind | Provides the role | Needs | Choose with |
|---|---|---|---|---|
smf_bottom_tabs | layout | Layout | a router | -m bottom_tabs |
bottom_tabs shows the main navigation of the app as tabs in a bar at the bottom. Features declare which of their routes are destinations of the main navigation, each with a label and an icon, such as Home with the home icon, and this module generates the widget around them.
What it adds to the app
The module adds lib/core/layout/app_shell.dart with AppShell, a Scaffold whose body is the screen of the selected destination. Below the screen is a bar with a tab for each destination: its icon over its label, below a hairline. The file draws the bar itself, with the widgets _TabBar and _Tab, in the colors of the theme of the app, so the file is where you change the look of the bar. The module adds no package to the app.
- The selected tab differs from the others only in its color, which it takes over a moment. No tab changes its size, and the bar has no indicator behind the selected icon.
- A tap on a tab selects its destination, with the tick of a selection on the device when it is another tab. The screen of the new destination fades in while it rises a little.
- A label is a text of the feature of its tab, so the tabs follow the language of the app.
- With a single destination there is nothing to switch to, so the bar is hidden and the screen fills the app.


The layout role adds lib/core/layout/destination.dart, whichever module provides the layout. It has Destination, the icon of an item and a function that returns its label, and appDestinations, the destinations of the app. A tab calls destination.label(context) each time it builds and keeps nothing of what it returned, so a label is in the language that the app is in. The role also requires every layout to keep destinations, currentIndex and onSelect of its AppShell public, so a test of the app can find a destination by its label and icon, select it with onSelect and read the selected one from currentIndex without knowing the module.
In the guide for coding agents of the app, the layout role describes the shell and the destinations under "Layout". The module adds that it draws the bar itself and hides it while the app has fewer than two destinations, to keep to five, where the bar takes its colors from, and what a tab says to a screen reader.
Change the look of the bar
The bar is not the NavigationBar of Material, but it takes its colors where that bar does: from the navigation bar theme of the app, NavigationBarTheme.of(context). For a color that the theme does not set, it reads the color scheme:
| Part of the bar | From the navigation bar theme | Otherwise, from the color scheme |
|---|---|---|
| The icon and the label of the selected tab | The color of iconTheme for a selected destination | primary |
| The icon and the label of the other tabs | The color of iconTheme | onSurfaceVariant |
| The background of the bar | backgroundColor | surface |
| The hairline above the bar | outlineVariant |
So for other colors, set them in the navigationBarTheme of the theme of the app. With material_theme, that is in lib/core/theme/app_theme.dart, where the selected tab gets the accent of the theme. The bar reads nothing else of that theme: the height, the indicator and the text style of a label there do not change it. A label has the style labelMedium of the text theme.
Everything else is in app_shell.dart: the size of an icon, the space between an icon and its label, how long a tab takes to change its color, and how the screen of a destination comes in. Two constants at the top of the file hold the height of the bar and the limit of the text size of a label:
/// The least height of the bar above the inset of the device. The bar grows
/// when a label needs more.
const double _barHeight = 64;
/// The most that the text size of the device scales the label of a tab. A
/// long press on a tab shows its label in a tooltip, at the full text size.
const double _maxLabelScale = 1.3;
Large text, screen readers and less motion
The bar gives what a navigation bar of Flutter would give:
- The text size of the device scales a label up to 1.3 times. A label keeps to one line and ends with an ellipsis when its tab is too narrow for it, and a long press on a tab shows the label in a tooltip at the full text size. The bar is at least 64 high above the inset of the device, and grows when a label needs more.
- Each tab is one button for a screen reader, with the label of its destination, selected or not. What you add to a tab, such as a badge, is not read unless the
Semanticsof the tab says it. - For a user who asks the device for less motion, the tab and the screen change at once.
How the router uses it
The module that provides the router builds the main navigation around the shell, with a branch for each destination in the order of the features. Each branch keeps its stack while another tab is selected. The app opens on the tab of its start screen, and an app without destinations has no main navigation. See Navigation.
Limits
The bar shows at most five destinations, as the Material Design guidelines advise. A feature whose destination does not fit is left out of the app with a warning, and with --strict, smf create stops instead.
Choosing it
smf create asks which module provides the layout before it asks about the router, and offers None as well. To choose it without the question:
smf create my_app -m home,bottom_tabs
The layout requires a router, which smf create adds when only one module provides it.