Tabs
A row of tabs that switches between views of the same content: one is selected, and a mark slides to the one selected next.
import com.pantrixui.components.container.tab.PantrixTabs
- Indicator. The style's
indicatormarks the selected tab: a line under it, a line over it, a pill behind it, or its colour alone. The indicator slides when the selection changes; in a theme with reduced motion it takes its place at once. - A shared row, or one that scrolls. The tabs share the row's width. With
scrollableeach is as wide as its label and the row scrolls sideways: it opens on the selected tab, and brings each tab selected after that to its middle. - Icon and badge.
TabItem.iconstands before the label andTabItem.badgeafter it: a count or a dot. Where the tab is narrow it is the label that is cut, and the badge stays whole. - Disabled tab. A press does nothing on a tab whose
enabledis false, the arrow keys skip it, and it is drawn in the theme's disabled text colour whatever colour is given. - Platform. A tab's least height is 48dp on Android, 44dp on iOS, and 40dp on the desktop and the web; a tab is never shorter than the platform's touch target.
- Colour. The row has no fill of its own: it sits on the page or on a surface.
TabsColorsgives the colours of the selected tab and of the others, of the indicator, and of the hairline below. - Automation. With a
tagKeythe row isTabs_<tagKey>and its tabs areTabs_<tagKey>.Tab_<key>. A selection is reported with the tab's index and whether a press or a key made it; never with its label.
Examples
Views of a profile
At its plainest: three tabs that share the row's width equally, and the selected view below them. The selected tab's index is the screen's state; it is kept with rememberSaveable, and is not lost when the screen rotates.
The tabs do not draw the view: the text below them is chosen here, with when. The default indicator is the line under the selected tab.
var tab by rememberSaveable { mutableIntStateOf(0) }
PantrixTabs(
tabs = listOf(
TabItem(label = "General", tagKey = "general"),
TabItem(label = "Security", tagKey = "security"),
TabItem(label = "Notifications", tagKey = "notifications")
),
selectedIndex = tab,
onSelect = { tab = it },
tagKey = "profile"
)
PantrixText(
text = when (tab) {
0 -> "Name, email and language"
1 -> "Password and two-step verification"
else -> "Which notifications you get"
},
color = PantrixTheme.colors.textSecondary
)
Tabs with badges
TabItem.badge puts a TabBadge after the label: a number where count is given, a dot where it is not. The "Activity" tab shows the number of unseen transactions; when the tab is opened the number goes to zero and the badge goes away. The dot on "Settings" says an update is waiting.
A screen reader reads the badge's contentDescription after the tab's name: "Activity, 3 new transactions". A number alone says nothing of what it counts; that is why the description has no default, and giving null is a decision.
var view by rememberSaveable { mutableIntStateOf(0) }
var unseen by rememberSaveable { mutableIntStateOf(3) }
PantrixTabs(
tabs = listOf(
TabItem(label = "Overview", tagKey = "overview"),
TabItem(
label = "Activity",
tagKey = "activity",
badge = if (unseen > 0) TabBadge(contentDescription = "$unseen new transactions", count = unseen) else null
),
TabItem(
label = "Settings",
tagKey = "settings",
badge = TabBadge(contentDescription = "Update available", tone = BadgeTone.Primary)
)
),
selectedIndex = view,
onSelect = { chosen ->
view = chosen
if (chosen == 1) unseen = 0
},
tagKey = "account"
)
PantrixText(
text = when (view) {
0 -> "Account overview: balance and recent transactions"
1 -> "Transactions of the last thirty days"
else -> "Notifications, language and privacy"
},
color = PantrixTheme.colors.textSecondary
)
The selected tab's indicator
The style says what marks the selected tab: TabsStyle(indicator = ...). The four rows show the same selection, each with the name of its indicator written above it; press another tab in one of them, and all four change together.
Underline is the default on every platform: a line along the selected tab's bottom edge. Top puts the same line along the top edge, for a row that sits below what it switches. Pill puts a pill in the primary role's pale fill behind the selected tab, and does not draw the hairline under the row: it is how iOS draws its segmented control, and here it picks a chart's period. None draws no indicator; the selected tab is marked by its colour alone.
var period by rememberSaveable { mutableIntStateOf(1) }
val caption = PantrixTheme.typography.caption
val quiet = PantrixTheme.colors.textSecondary
PantrixText(text = "Underline", style = caption, color = quiet)
PantrixTabs(tabs = PERIODS, selectedIndex = period, onSelect = { period = it }, tagKey = "underline")
PantrixText(text = "Top", style = caption, color = quiet)
PantrixTabs(
tabs = PERIODS,
selectedIndex = period,
onSelect = { period = it },
style = TabsStyle(indicator = TabIndicator.Top),
tagKey = "top"
)
PantrixText(text = "Pill", style = caption, color = quiet)
PantrixTabs(
tabs = PERIODS,
selectedIndex = period,
onSelect = { period = it },
style = TabsStyle(indicator = TabIndicator.Pill),
tagKey = "period"
)
PantrixText(text = "None", style = caption, color = quiet)
PantrixTabs(
tabs = PERIODS,
selectedIndex = period,
onSelect = { period = it },
style = TabsStyle(indicator = TabIndicator.None),
tagKey = "none"
)
private val PERIODS = listOf(
TabItem(label = "Day", tagKey = "day"),
TabItem(label = "Week", tagKey = "week"),
TabItem(label = "Month", tagKey = "month")
)
Categories with icons in a row that scrolls
More categories than the row has room for, each with its icon. With scrollable = true each tab is as wide as its label and the row scrolls sideways. The selected tab comes to the middle of the row, so that the tabs on both sides of it can be seen and pressed.
The count on "Inbox" is over the largest number that is written: "99+". The last category is disabled: it cannot be pressed, and the arrow keys skip it.
var category by rememberSaveable { mutableIntStateOf(0) }
PantrixTabs(
tabs = CATEGORIES,
selectedIndex = category,
onSelect = { category = it },
scrollable = true,
tagKey = "categories"
)
private val CATEGORIES = listOf(
TabItem(
label = "Inbox",
icon = PantrixIcons.Inbox,
tagKey = "inbox",
badge = TabBadge(contentDescription = "More than 99 unread messages", count = 128)
),
TabItem(label = "Starred", icon = PantrixIcons.Star, tagKey = "starred"),
TabItem(label = "Folders", icon = PantrixIcons.Folder, tagKey = "folders"),
TabItem(label = "People", icon = PantrixIcons.Person, tagKey = "people"),
TabItem(label = "Calendar", icon = PantrixIcons.Calendar, tagKey = "calendar"),
TabItem(label = "Search", icon = PantrixIcons.Search, tagKey = "search"),
TabItem(label = "Archive", icon = PantrixIcons.Folder, enabled = false, tagKey = "archive")
)
With pages that are swiped
The app changes the selection as much as the user does. Here the selected tab is a pager's page: swipe the page sideways, and the indicator slides to the new tab. When a tab is pressed the tabs only report which one; it is the code in onSelect that turns the page.
val pager = rememberPagerState { 3 }
val scope = rememberCoroutineScope()
PantrixTabs(
tabs = listOf(
TabItem(label = "Photos", tagKey = "photos"),
TabItem(label = "Videos", tagKey = "videos"),
TabItem(label = "Documents", tagKey = "documents")
),
selectedIndex = pager.currentPage,
onSelect = { scope.launch { pager.animateScrollToPage(it) } },
tagKey = "library"
)
HorizontalPager(state = pager, modifier = Modifier.height(96.dp)) { page ->
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PantrixText(
text = when (page) {
0 -> "248 photos"
1 -> "12 videos"
else -> "37 documents"
}
)
}
}
With no tab selected
When selectedIndex is an index outside the tabs, no tab is selected and no indicator is drawn: the app is in a place that is no tab's view. Press the button: the selection goes. Press a tab: it comes back.
var view by rememberSaveable { mutableIntStateOf(0) }
PantrixTabs(
tabs = listOf(
TabItem(label = "Feed", tagKey = "feed"),
TabItem(label = "Explore", tagKey = "explore"),
TabItem(label = "Saved", tagKey = "saved")
),
selectedIndex = view,
onSelect = { view = it },
tagKey = "home"
)
PantrixButton(
text = "Open search results",
onClick = { view = -1 },
variant = ButtonVariant.Outline,
size = ButtonSize.Small,
tagKey = "tabs_open_search"
)
PantrixText(
text = if (view < 0) "Search results are open: no tab is selected" else "A tab's view is open",
color = PantrixTheme.colors.textSecondary
)
In its own colours
With colors the row takes the colours it is given, not the theme's. The first row is given the colour of the selected tab and of the line; the tabs that are not selected and the hairline below stay in the theme's colours. The badge of the "Pending" tab, too, is filled with the colour it is given and not with its tone's.
The second row is a row of pills, and only the pill's fill is given. That fill is the colour of one of the theme's roles, so the theme pairs the selected label's colour with it. On a fill no role has, selectedContent is given as well.
val own = ownColors()
var order by rememberSaveable { mutableIntStateOf(0) }
var period by rememberSaveable { mutableIntStateOf(0) }
PantrixTabs(
tabs = listOf(
TabItem(
label = "Pending",
tagKey = "pending",
badge = TabBadge(
contentDescription = "2 pending orders",
count = 2,
colors = BadgeColors(container = own.color, content = own.onColor)
)
),
TabItem(label = "Shipped", tagKey = "shipped"),
TabItem(label = "Delivered", tagKey = "delivered")
),
selectedIndex = order,
onSelect = { order = it },
colors = TabsColors(selectedContent = own.color, indicator = own.color),
tagKey = "orders"
)
PantrixTabs(
tabs = PERIODS,
selectedIndex = period,
onSelect = { period = it },
style = TabsStyle(indicator = TabIndicator.Pill),
colors = TabsColors(indicator = PantrixTheme.colors.success.container),
tagKey = "success_pill"
)
private val PERIODS = listOf(
TabItem(label = "Day", tagKey = "day"),
TabItem(label = "Week", tagKey = "week"),
TabItem(label = "Month", tagKey = "month")
)
API
tabs
A row of tabs that switches between views of the same content: one is selected, and a mark slides to the one selected next.
| Parameter | Type | Default | What it is |
|---|---|---|---|
tabs | List<TabItem> | the tabs, in order. | |
selectedIndex | Int | the selected tab's index. Outside tabs, no tab is selected and no mark is drawn. | |
onSelect | (index: Int) -> Unit | called with the index of the tab the user chose, by a press or by the keys, whether or not it is selected already. The row selects nothing by itself: a tab is selected when selectedIndex says so. | |
modifier | Modifier | Modifier | the row's modifier. The row fills the width it is given. |
scrollable | Boolean | false | false shares the row's width between the tabs. True makes each as wide as its label, at least the style's minTabWidth, in a row that scrolls sideways and keeps the selected tab in its middle. |
style | TabsStyle? | null | this call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform. |
colors | TabsColors? | null | this row's own colours: the ones it sets, with the theme's for the rest. A disabled tab keeps the theme's. |
tagKey | String? | null | the row's automation key: the row is Tabs_<tagKey>, and its tabs are named inside it, Tabs_<tagKey>.Tab_<key>, so that two rows with the same tabs are told apart. A selection is reported as a Tabs event that carries the tab's index and whether a press or a key chose it, and never its label. |
TabsStyle
PantrixTabs' geometry, and how it marks the selected tab.
| Parameter | Type | Default | What it is |
|---|---|---|---|
height | Dp | Dp.Unspecified | The least a tab is tall: 48dp on Android, 44dp on iOS, 40dp on desktop and the web. A tab is never shorter than the platform's touch target, whatever this says. |
horizontalPadding | Dp | Dp.Unspecified | Inside a tab, before its icon or label and after its label. |
indicatorHeight | Dp | Dp.Unspecified | How thick an TabIndicator.Underline or a TabIndicator.Top line is. |
minTabWidth | Dp | Dp.Unspecified | The least a scrollable tab is wide. |
edgePadding | Dp | Dp.Unspecified | Before the first scrollable tab and after the last. |
iconSize | Dp | Dp.Unspecified | The size of a tab's icon. |
iconGap | Dp | Dp.Unspecified | Between a tab's icon and its label, and between its label and its badge. |
indicator | TabIndicator? | null | What marks the selected tab. |
indicatorPadding | Dp | Dp.Unspecified | Between a TabIndicator.Pill and the edges of the tab it marks. |
TabsColors
What PantrixTabs is painted with, for a row that does not take the theme's: a colour that is set is used, and one that is left out stays the theme's role.
| Parameter | Type | Default | What it is |
|---|---|---|---|
selectedContent | Color | Color.Unspecified | The label and the icon of the selected tab. The theme's primary, and on a pill the primary role's onContainer, the colour paired with the pill's fill. A pill given an indicator that is one of the theme's own colours brings the colour the theme pairs with it; for one no role has, give this with it. |
unselectedContent | Color | Color.Unspecified | The label and the icon of the other tabs. The theme's textSecondary. |
indicator | Color | Color.Unspecified | What marks the selected tab: the line under or over it, or the pill behind it. The theme's primary, and for a pill the primary role's container. |
divider | Color | Color.Unspecified | The hairline under the row, which a row of pills does not draw. The theme's divider on the theme's own grounds. A row of tabs has no fill of its own: on a fill, where that line would not read, a share of what is written there. |
Accessibility
- Keyboard. The row is one stop for the Tab key, on the selected tab. The arrow keys select the next and the previous tab, the other way round in a right-to-left layout; Home and End the first and the last. While a tab has the focus, the focus goes with the selection.
- Screen reader. Each tab is a tab that says whether it is selected, and together they are one group. The badge's description is read after the label, as a part of the tab.