Navigation Rail
The app's top-level destinations down the start edge of a wide window, with the one the app is at marked: the navigation bar's place on a tablet, an open foldable or a desktop window.
import com.pantrixui.components.navigation.navigationrail.PantrixNavigationRail
- The same destinations as a bar.
itemsis a list ofNavigationItem: the icon, the label, the badge, another icon while selected and a destination that cannot be pressed are as in a bar. A rail takes three to seven destinations. - The state is the screen's.
selectedis the key of the current destination, andonSelectreports that of the one that is pressed; a press on the current destination is reported too. Withselectednull, no destination is marked. - Header.
headerstands over the destinations, in the middle of the rail: a button that opens the menu, or the screen's main action. - Destinations that scroll. Where the destinations are taller than the rail they scroll by themselves; the header stays where it is.
- Height. The rail is as tall as the room it is given, and that room has to have an end: a scaffold's side bar has one; inside a page that scrolls it takes a box of a set height.
- A pill, or colour alone. The current destination is marked by the pill behind its icon; by its colour alone where the
indicatorfield ofNavigationRailStyleisNavigationIndicator.None. - The same on every platform. 80dp wide, with a hairline along its end edge; the destinations are 56dp tall and 4dp apart.
stylechanges the sizes,labelStylethe label's type. - Colours.
NavigationColorsis the same class as the bar's: one set of colours paints both components. - System bars.
insetsis the room the rail keeps clear at its top, its bottom and its start edge: the fill runs behind that room, and the destinations stay out of it.
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 destinations stand one under another from the top; each is as wide as the rail, so a press beside the icon is a press on the destination too.
The rail is as tall as the frame (240dp): in a place that has no height of its own, inside a page that scrolls, it has to be given one.
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") }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(240.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = destinations,
selected = where,
onSelect = { where = it },
tagKey = "plain"
)
PantrixText(
text = "Open destination: ${destinations.first { it.key == where }.label}",
modifier = Modifier.padding(PantrixTheme.spacing.lg),
color = PantrixTheme.colors.textSecondary
)
}
}
Three destinations under a menu button
header stands over the destinations, in the middle of the rail: here, a button that opens the menu. Press it and the area beside the rail counts.
The destinations are a mail app's. The badge on "Inbox" counts the unread: when the destination is opened the count goes to zero and the badge goes away. A screen reader reads the badge after the destination's name: "Inbox, 3 unread".
var where by rememberSaveable { mutableStateOf("starred") }
var unread by rememberSaveable { mutableIntStateOf(3) }
var menus by rememberSaveable { mutableIntStateOf(0) }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(440.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = listOf(
NavigationItem(key = "starred", label = "Starred", icon = PantrixIcons.Star),
NavigationItem(
key = "inbox",
label = "Inbox",
icon = PantrixIcons.Inbox,
badge = if (unread > 0) NavigationBadge("$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
},
header = {
PantrixIconButton(
imageVector = PantrixIcons.Menu,
contentDescription = "Open menu",
onClick = { menus++ },
tagKey = "rail_menu"
)
},
tagKey = "mail"
)
PantrixText(
text = when (where) {
"starred" -> "Starred messages"
"inbox" -> "Inbox: nothing left unread"
else -> "Profile and settings"
} + if (menus > 0) "\nTimes the menu was opened: $menus" else "",
modifier = Modifier.padding(PantrixTheme.spacing.lg),
color = PantrixTheme.colors.textSecondary
)
}
}
The destinations scroll in a short rail
Seven destinations in a rail of 220dp: the most a rail takes, in a place too short to show them all. The destinations scroll by themselves; the button in the header does not scroll, it stays where it is. Scroll the rail up: it goes down as far as "About".
The badge on "Folders" is a dot, the 128 on "Profile" becomes "99+", and "Calendar" cannot be pressed.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(220.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = SEVEN,
selected = where,
onSelect = { where = it },
header = {
PantrixIconButton(
imageVector = PantrixIcons.Menu,
contentDescription = "Open menu",
onClick = {},
tagKey = "short_menu"
)
},
tagKey = "short"
)
}
}
private val SEVEN = 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)
),
NavigationItem(key = "inbox", label = "Inbox", icon = PantrixIcons.Inbox),
NavigationItem(key = "info", label = "About", icon = PantrixIcons.Info)
)
By colour alone
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. The press and the focus ring are still drawn in the pill's room.
The frame is 440dp: the seven destinations fit at their preset sizes and the rail does not scroll.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(440.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = SEVEN,
selected = where,
onSelect = { where = it },
style = NavigationRailStyle(indicator = NavigationIndicator.None),
tagKey = "tint"
)
}
}
private val SEVEN = 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)
),
NavigationItem(key = "inbox", label = "Inbox", icon = PantrixIcons.Inbox),
NavigationItem(key = "info", label = "About", icon = PantrixIcons.Info)
)
Style: width, gaps and the label's type
style changes the rail's sizes, labelStyle the type the labels are set in; only the fields that are given change. The first rail is the preset: 80dp wide, its destinations 56dp tall and 4dp apart. The second is 96dp wide, its destinations 64dp tall and 12dp apart, 16dp in from the top and the bottom; its labels are set in caption.
The second rail has no line along its end edge either: divider = false.
val destinations = SEVEN.take(4)
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(340.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = destinations,
selected = where,
onSelect = { where = it },
tagKey = "usual"
)
PantrixNavigationRail(
items = destinations,
selected = where,
onSelect = { where = it },
labelStyle = PantrixTheme.typography.caption,
style = NavigationRailStyle(
width = 96.dp,
itemMinHeight = 64.dp,
itemSpacing = 12.dp,
verticalPadding = 16.dp,
divider = false
),
tagKey = "wide"
)
}
}
private val SEVEN = 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)
),
NavigationItem(key = "inbox", label = "Inbox", icon = PantrixIcons.Inbox),
NavigationItem(key = "info", label = "About", icon = PantrixIcons.Info)
)
On a role's fill, with its header
Where container is one of the theme's role colours, the theme knows what reads on it: content was not given, and the destinations were written in the colour the theme pairs with the role's. The destination that cannot be pressed is written in a share of that colour, and the line along the edge comes from it too.
The icon and the text in the header name no colour either: on the theme's own fill they would be in their own default colours, and here they are in the colour that is written on the rail.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(340.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = SEVEN.take(4),
selected = where,
onSelect = { where = it },
colors = NavigationColors(container = PantrixTheme.colors.primary.color),
header = {
PantrixIcon(imageVector = PantrixIcons.Star, contentDescription = null)
PantrixText(text = "Acme", style = PantrixTheme.typography.footnote)
},
tagKey = "role"
)
}
}
private val SEVEN = 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)
),
NavigationItem(key = "inbox", label = "Inbox", icon = PantrixIcons.Inbox),
NavigationItem(key = "info", label = "About", icon = PantrixIcons.Info)
)
In its own colours
A rail in a colour the theme does not have: the app's own purple. The fill (container) and the colour that reads on it (content) are given together; the other destinations, the one that cannot be pressed and the line along the edge come from that colour. The pill and the icon on it were given too (selectedContainer, selectedContent).
The same NavigationColors can be given to a PantrixNavigationBar too: the bar and the rail are painted with one set of colours.
val own = ownColors()
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(440.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = SEVEN,
selected = where,
onSelect = { where = it },
colors = NavigationColors(
container = own.color,
content = own.onColor,
selectedContainer = own.container,
selectedContent = own.onContainer
),
tagKey = "own"
)
}
}
private val SEVEN = 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)
),
NavigationItem(key = "inbox", label = "Inbox", icon = PantrixIcons.Inbox),
NavigationItem(key = "info", label = "About", icon = PantrixIcons.Info)
)
Behind the system bars
insets is the room the rail keeps clear at its top, its bottom and its start edge: the fill runs behind that room, and the header and the destinations stay out of it. On a real screen PantrixScaffold gives the insets to its side bar, and the rail runs behind the system bars.
There is no scaffold here: the 24dp strip at the top stands in for the system's status bar. The rail's fill goes on behind the strip, and the menu button starts under it.
var where by rememberSaveable { mutableStateOf("search") }
PantrixSurface(modifier = Modifier.fillMaxWidth().height(340.dp), border = PantrixTheme.colors.divider) {
Row {
PantrixNavigationRail(
items = SEVEN.take(4),
selected = where,
onSelect = { where = it },
header = {
PantrixIconButton(imageVector = PantrixIcons.Menu, contentDescription = "Open menu", onClick = {}, tagKey = "menu")
},
insets = PaddingValues(top = 24.dp),
tagKey = "insets"
)
}
// A strip in the place of the system's status bar: the rail's fill goes on behind it.
Box(
modifier = Modifier
.fillMaxWidth()
.height(24.dp)
.background(PantrixTheme.colors.textSecondary.copy(alpha = 0.16f))
)
}
private val SEVEN = 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)
),
NavigationItem(key = "inbox", label = "Inbox", icon = PantrixIcons.Inbox),
NavigationItem(key = "info", label = "About", icon = PantrixIcons.Info)
)
API
items
The app's top-level destinations down the start edge of a wide window, with the one the app is at marked: the navigation bar's place on a tablet, an open foldable or a desktop window.
| Parameter | Type | Default | What it is |
|---|---|---|---|
items | List<NavigationItem> | the destinations, in order: three to seven 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 rail. |
labelStyle | TextStyle | PantrixTheme.typography.footnote | the type the labels are set in: the theme's footnote unless given. Their colour is the rail's. |
style | NavigationRailStyle? | 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. |
header | (@Composable ColumnScope.() -> Unit)? | null | over the destinations, in the middle of the rail's width. A text or an icon in it that names no colour of its own has its own default on the theme's fill, and what is written on the rail on a fill of the caller's. |
insets | PaddingValues | PaddingValues() | the room the rail keeps clear at its top, its bottom and its start, with its fill behind it: what a scaffold gives its side bar. |
tagKey | String? | null | a locale-independent key for automation: the rail's tag is NavigationRail_<tagKey>, and its destinations are named inside it, NavigationRail_<tagKey>.Item_<destination's key>. |
NavigationRailStyle
PantrixNavigationRail's geometry, and how it marks the destination the app is at.
| Parameter | Type | Default | What it is |
|---|---|---|---|
width | Dp | Dp.Unspecified | How wide the rail is, beside the insets it keeps clear. |
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. |
itemMinHeight | Dp | Dp.Unspecified | The least a destination is tall. It is never lower than the platform's touch target, and grows with a larger type. |
itemSpacing | Dp | Dp.Unspecified | Between one destination and the next. |
headerGap | Dp | Dp.Unspecified | Between the header and the first destination. |
verticalPadding | Dp | Dp.Unspecified | Over the header, or the first destination, and under the last. |
indicator | NavigationIndicator? | null | What marks the current destination. |
divider | Boolean? | null | Whether a hairline runs along the rail's end edge, inside its width. |
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 and right to left. A destination is read as a tab: its name, what its badge says, and whether it is selected. In languages read from right to left the rail is at the right edge, and its line moves to its left edge.