Skip to main content

bottom_tabs

PackageKindProvides the roleNeedsChoose with
smf_bottom_tabslayoutLayouta 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 bar below the start screen in the light theme: the tabs Home and Settings, each an icon over its label, below a hairline. Home is the selected tab, in green.The bar below the settings screen in the dark theme, in Ukrainian: the labels of the tabs are in Ukrainian, and the second tab is the selected one.

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 barFrom the navigation bar themeOtherwise, from the color scheme
The icon and the label of the selected tabThe color of iconTheme for a selected destinationprimary
The icon and the label of the other tabsThe color of iconThemeonSurfaceVariant
The background of the barbackgroundColorsurface
The hairline above the baroutlineVariant

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:

lib/core/layout/app_shell.dart
/// 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 Semantics of 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.