Navigation Bar
The app's top-level destinations along the bottom of the screen, three to five of them, with the one the app is at marked.
import com.pantrixui.components.navigation.navigationbar.PantrixNavigationBar
- The state is the screen's. The bar is given the key of the current destination (
selected), and reports the key of a destination that is pressed (onSelect). A press on the current destination is reported too: an app commonly takes that destination back to its top. Withselectednull, no destination is marked. - A pill, or colour alone. The current destination is marked by a pill behind its icon; by its colour alone where the
indicatorfield ofNavigationBarStyleisNavigationIndicator.None. - Badge.
NavigationBadgeputs a count or a dot on the destination's icon. The badge's top is level with the icon's: it does not run out of the bar. The count is capped as a badge's is: "99+". - Another icon while selected.
NavigationItem.selectedIconis drawn in place oficonwhile the app is at that destination. - A destination that cannot be pressed.
enabled = falsedraws the destination in the theme's disabled text colour; the destination takes no press. - The same on every platform. 64dp tall, under a hairline; a pill of 56×28dp, a 20dp icon and a label set in
footnote.stylechanges the sizes,labelStylethe label's type. - Colours.
NavigationColorstakes the fill, what is written on it, the pill, the icon on the pill and the colour that marks where there is no pill, each on its own.PantrixNavigationRailtakes the same colours and the same list of destinations. - System bar.
insetsis the room the bar keeps clear along the bottom edge and at its sides: the fill runs behind that room, and the destinations stay out of it. - Upright bar.
orientation = NavigationBarOrientation.Verticallays the destinations one above another in a capsule of the bar's fill: as the tabs in the system's vertical bar on the iPhone Duo's outer screen.PantrixScaffoldputs it at the bottom of the column there by itself. In an upright bar the destinations have their icons alone;showLabels = truewrites their names too. Along the bottom,showLabels = falseleaves the icons alone as well. - Destination without a label. Its name is told to a screen reader and shows in a tooltip while a pointer rests on it, while it has the keyboard's focus and after a long press: above the destination along the bottom, at its start in an upright bar.
- Automation. The destinations of a bar that is given a
tagKeyare named inside it:NavigationBar_main.Item_inbox.
Examples
Three destinations
At its plainest: the list of destinations (items), the key of the current destination (selected) and onSelect, which is told the key of the destination that is pressed. The bar selects nothing by itself; the screen holds where the app is. The destinations share the bar's width.
Press a destination, then press the same one again: that is reported too. The line above counts it; a real app takes that destination back to its top.
val destinations = listOf(
NavigationItem(key = "files", label = "Files", icon = PantrixIcons.Folder),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "profile", label = "Profile", icon = PantrixIcons.Person)
)
var where by rememberSaveable { mutableStateOf("files") }
var pressedAgain by rememberSaveable { mutableIntStateOf(0) }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
Column {
PantrixText(
text = "Open destination: ${destinations.first { it.key == where }.label}. " +
"Times the same destination was pressed again: $pressedAgain.",
modifier = Modifier.padding(PantrixTheme.spacing.lg),
color = PantrixTheme.colors.textSecondary
)
PantrixNavigationBar(
items = destinations,
selected = where,
onSelect = { key -> if (key == where) pressedAgain++ else where = key },
tagKey = "plain"
)
}
}
Three destinations, with a count on one
The three destinations of a mail app. The badge on "Inbox" counts the unread: NavigationBadge(contentDescription, count). When the destination is opened the count goes to zero and the badge goes away: with nothing left to count, badge is given null. "Starred" has a selectedIcon: while it is selected, that is drawn in place of the star. An app gives the icon's filled form there; PantrixIcons has no filled forms, so another icon stands here.
The badge's contentDescription is required and is read after the destination's name: "Inbox, 3 unread". A number alone does not say what it counts; giving null is a decision, made where the name says it already.
var where by rememberSaveable { mutableStateOf("starred") }
var unread by rememberSaveable { mutableIntStateOf(3) }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
Column {
PantrixText(
text = when (where) {
"starred" -> "Starred messages"
"inbox" -> "Inbox: nothing left unread"
else -> "Profile and settings"
},
modifier = Modifier.padding(PantrixTheme.spacing.lg),
color = PantrixTheme.colors.textSecondary
)
PantrixNavigationBar(
items = listOf(
NavigationItem(
key = "starred",
label = "Starred",
icon = PantrixIcons.Star,
selectedIcon = PantrixIcons.Success
),
NavigationItem(
key = "inbox",
label = "Inbox",
icon = PantrixIcons.Inbox,
badge = if (unread > 0) {
NavigationBadge(contentDescription = "$unread unread", count = unread)
} else {
null
}
),
NavigationItem(key = "profile", label = "Profile", icon = PantrixIcons.Person)
),
selected = where,
onSelect = { key ->
where = key
if (key == "inbox") unread = 0
},
tagKey = "mail"
)
}
}
With a pill: five destinations, one that cannot be pressed
Five destinations: the most a bar takes. The current destination is marked by the pill behind its icon (NavigationIndicator.Pill): that is the default on every platform, written out here. The press and the focus ring are drawn in the pill's room too, not over the whole destination.
The badge on "Folders" is a dot (count was not given); the 128 on "Profile" is more than the largest number that is written, and becomes "99+". "Calendar" cannot be pressed: enabled = false.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
style = NavigationBarStyle(indicator = NavigationIndicator.Pill),
tagKey = "pill"
)
}
private val FIVE = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "folders",
label = "Folders",
icon = PantrixIcons.Folder,
badge = NavigationBadge(contentDescription = "New files")
),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "calendar", label = "Calendar", icon = PantrixIcons.Calendar, enabled = false),
NavigationItem(
key = "profile",
label = "Profile",
icon = PantrixIcons.Person,
badge = NavigationBadge(contentDescription = "128 notifications", count = 128)
)
)
By colour alone
The same bar with NavigationIndicator.None: no pill is drawn, the current destination's icon and label are in the theme's primary colour, and the others in the secondary text colour. A tab bar on iOS marks its destination this way; no platform's preset chooses it, and whoever wants it gives a style.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
style = NavigationBarStyle(indicator = NavigationIndicator.None),
tagKey = "tint"
)
}
private val FIVE = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "folders",
label = "Folders",
icon = PantrixIcons.Folder,
badge = NavigationBadge(contentDescription = "New files")
),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "calendar", label = "Calendar", icon = PantrixIcons.Calendar, enabled = false),
NavigationItem(
key = "profile",
label = "Profile",
icon = PantrixIcons.Person,
badge = NavigationBadge(contentDescription = "128 notifications", count = 128)
)
)
Style: other sizes and the label's type
style changes the bar's sizes, labelStyle the type the labels are set in. The preset bar is 64dp tall on every platform; Android's own bar is 80dp with a larger pill, and a tab bar on iOS is 49pt with no pill. Neither is the preset, but a style can ask for either.
The bar above is 80dp, with a pill of 64×32dp and a 24dp icon. The one below is 49dp, has no pill, has its labels set in caption and less room between an icon and its label (labelGap = 2.dp). Only the fields that are given change. A destination is never lower than the platform's touch target.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
style = NavigationBarStyle(
height = 80.dp,
indicatorWidth = 64.dp,
indicatorHeight = 32.dp,
iconSize = 24.dp
),
tagKey = "tall"
)
}
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
labelStyle = PantrixTheme.typography.caption,
style = NavigationBarStyle(height = 49.dp, labelGap = 2.dp, indicator = NavigationIndicator.None),
tagKey = "low"
)
}
private val FIVE = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "folders",
label = "Folders",
icon = PantrixIcons.Folder,
badge = NavigationBadge(contentDescription = "New files")
),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "calendar", label = "Calendar", icon = PantrixIcons.Calendar, enabled = false),
NavigationItem(
key = "profile",
label = "Profile",
icon = PantrixIcons.Person,
badge = NavigationBadge(contentDescription = "128 notifications", count = 128)
)
)
The current destination in a role's colours
Only the current destination's colour has changed; the rest is the theme's. selectedContainer paints the pill; given one of the theme's role colours, the colour of the icon on it comes from that role too, unless selectedContent is given as well. selectedTint is the colour that marks the destination in a style that draws no pill.
Each has its own place: giving the one changes nothing where the other is drawn. The same colors were given to two bars, one with a pill and one without.
val success = PantrixTheme.colors.success
val colors = NavigationColors(selectedContainer = success.container, selectedTint = success.color)
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
colors = colors,
tagKey = "role"
)
}
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
style = NavigationBarStyle(indicator = NavigationIndicator.None),
colors = colors,
tagKey = "role_tint"
)
}
private val FIVE = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "folders",
label = "Folders",
icon = PantrixIcons.Folder,
badge = NavigationBadge(contentDescription = "New files")
),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "calendar", label = "Calendar", icon = PantrixIcons.Calendar, enabled = false),
NavigationItem(
key = "profile",
label = "Profile",
icon = PantrixIcons.Person,
badge = NavigationBadge(contentDescription = "128 notifications", count = 128)
)
)
In its own colours
A bar in a colour the theme does not have: the app's own purple. The theme cannot know what reads on this fill; with the fill (container), the colour on it (content) is given too. The other destinations are written in that colour, the one that cannot be pressed in a share of it, and the line along the top comes from it as well.
The pill and the icon on it were given too (selectedContainer, selectedContent). A style that draws no pill does not use them: the current destination is in the bar's text colour, and the others in a share of it.
val own = ownColors()
var where by rememberSaveable { mutableStateOf("folders") }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
colors = NavigationColors(
container = own.color,
content = own.onColor,
selectedContainer = own.container,
selectedContent = own.onContainer
),
tagKey = "own"
)
}
private val FIVE = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "folders",
label = "Folders",
icon = PantrixIcons.Folder,
badge = NavigationBadge(contentDescription = "New files")
),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "calendar", label = "Calendar", icon = PantrixIcons.Calendar, enabled = false),
NavigationItem(
key = "profile",
label = "Profile",
icon = PantrixIcons.Person,
badge = NavigationBadge(contentDescription = "128 notifications", count = 128)
)
)
Behind the system bar
insets is the room the bar keeps clear along the bottom edge and at its sides: the fill runs behind that room, and the destinations stay out of it. On a real screen PantrixScaffold gives the insets (bottomBar = { insets -> … }) and the system's navigation bar sits over the bar's fill. That is how the app's own bottom bar gets its insets.
There is no scaffold here: 24dp stand in for the insets, and the line inside them for the system's gesture bar. The destinations stay above the line, and the bar's fill goes down under it.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
Box(contentAlignment = Alignment.BottomCenter) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
insets = PaddingValues(bottom = 24.dp),
tagKey = "insets"
)
// A line in the place of the system's gesture bar: over the bar's fill, under the destinations.
Box(
modifier = Modifier
.padding(bottom = 10.dp)
.size(width = 120.dp, height = 4.dp)
.background(PantrixTheme.colors.textSecondary, PantrixTheme.shapes.full)
)
}
}
private val FIVE = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "folders",
label = "Folders",
icon = PantrixIcons.Folder,
badge = NavigationBadge(contentDescription = "New files")
),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "calendar", label = "Calendar", icon = PantrixIcons.Calendar, enabled = false),
NavigationItem(
key = "profile",
label = "Profile",
icon = PantrixIcons.Person,
badge = NavigationBadge(contentDescription = "128 notifications", count = 128)
)
)
Upright bar
The bar stands upright: the destinations one above another, in a capsule of the bar's fill with a thin line around it. On the iPhone Duo's outer screen the system keeps a column for such bars along the right edge; PantrixScaffold puts the navigation at the bottom of that column and tells it ScaffoldNavigationPlacement.VerticalBar (the "Scaffold" page has an example).
The one on the left has its icons alone, as UIKit's tabs in that column: rest on a destination or press it long, and its name shows in a tooltip at its start; a screen reader says the name too. The one on the right writes the names as well, with showLabels = true, and is as wide as its longest name. The two share one state: press a destination in one and the other marks it too.
var where by rememberSaveable { mutableStateOf("search") }
Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.xxl)) {
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
orientation = NavigationBarOrientation.Vertical,
tagKey = "upright_icons"
)
PantrixNavigationBar(
items = FIVE,
selected = where,
onSelect = { where = it },
orientation = NavigationBarOrientation.Vertical,
showLabels = true,
tagKey = "upright_labels"
)
}
private val FIVE = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "folders",
label = "Folders",
icon = PantrixIcons.Folder,
badge = NavigationBadge(contentDescription = "New files")
),
NavigationItem(key = "search", label = "Search", icon = PantrixIcons.Search),
NavigationItem(key = "calendar", label = "Calendar", icon = PantrixIcons.Calendar, enabled = false),
NavigationItem(
key = "profile",
label = "Profile",
icon = PantrixIcons.Person,
badge = NavigationBadge(contentDescription = "128 notifications", count = 128)
)
)
API
items
The app's top-level destinations along the bottom of the screen, three to five of them, with the one the app is at marked.
| Parameter | Type | Default | What it is |
|---|---|---|---|
items | List<NavigationItem> | the destinations, in order: three to five of them. | |
selected | String? | the key of the destination the app is at, or null where it is at none of them. | |
onSelect | (key: String) -> Unit | told the key of a destination that is pressed, the current one's too. | |
modifier | Modifier | Modifier | applied to the bar. |
orientation | NavigationBarOrientation | NavigationBarOrientation.Horizontal | which way the bar runs: along the bottom, or upright in the system's vertical bar. |
showLabels | Boolean | orientation == NavigationBarOrientation.Horizontal | whether each destination shows its label under its icon: along the bottom it does unless told not to, upright it does not unless told to. |
labelStyle | TextStyle | PantrixTheme.typography.footnote | the type the labels are set in: the theme's footnote unless given. Their colour is the bar's. |
style | NavigationBarStyle? | null | this call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform. |
colors | NavigationColors? | null | this call's own colours. Only the ones it sets change; the rest stay the theme's. |
insets | PaddingValues | PaddingValues() | the room the bar keeps clear along the bottom edge and at its sides, with its fill behind it: what a scaffold gives its bottom bar. An upright bar keeps it clear around its capsule, with nothing behind it. |
tagKey | String? | null | a locale-independent key for automation: the bar's tag is NavigationBar_<tagKey>, and its destinations are named inside it, NavigationBar_<tagKey>.Item_<destination's key>. |
NavigationBarStyle
PantrixNavigationBar's geometry, and how it marks the destination the app is at.
| Parameter | Type | Default | What it is |
|---|---|---|---|
height | Dp | Dp.Unspecified | How tall the bar is, over the insets it keeps clear. A destination is never lower than the platform's touch target, and grows with a larger type. |
indicatorWidth | Dp | Dp.Unspecified | How wide the pill behind the current destination's icon is, and the room a press is drawn in. |
indicatorHeight | Dp | Dp.Unspecified | How tall that pill is. |
iconSize | Dp | Dp.Unspecified | A destination's icon. |
labelGap | Dp | Dp.Unspecified | Between the pill's room and the label under it. |
indicator | NavigationIndicator? | null | What marks the current destination. |
divider | Boolean? | null | Whether a hairline runs along the bar's top edge, inside its height; upright, around its capsule. |
uprightIndicatorWidth | Dp | Dp.Unspecified | How wide the pill is in an upright bar, and the room a press is drawn in there. A destination there is as wide as its pill and its label. |
NavigationColors
What a navigation bar or a navigation rail is painted with, where it does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's. The two take the same colours, so a screen that shows one or the other by the window's width paints both with one set.
| Parameter | Type | Default | What it is |
|---|---|---|---|
container | Color | Color.Unspecified | The fill, which runs behind the insets that are kept clear. The theme's surface. |
content | Color | Color.Unspecified | What is written on the fill: the label of the destination the app is at, under its pill, and the icons and labels of the others. On the theme's own fill the first is the theme's textPrimary and the others its textSecondary. On a fill of the caller's, and where this is given, all of them are this colour, and where no pill marks the current destination, the others are a share of it. |
selectedContainer | Color | Color.Unspecified | The pill behind the current destination's icon, where the style draws one. The primary role's container. |
selectedContent | Color | Color.Unspecified | The current destination's icon on its pill: the colour that reads on selectedContainer. The primary role's onContainer. |
selectedTint | Color | Color.Unspecified | The current destination's icon and its label where the style draws no pill, and colour alone marks it: a colour that reads on container. The primary role's color on the theme's own fill, and content on a fill of the caller's, where nothing says that the brand's colour reads. |
divider | Color | Color.Unspecified | The hairline along a bar's top edge or a rail's end edge, where the style draws one. The theme's divider on the theme's own grounds. On a fill, where that line would not read, a share of content: the edge a subtle button has. |
Accessibility
- Screen reader. A destination is read as a tab: its name, what its badge says, and whether it is selected. "Inbox, 3 unread".